export class Peer {
  onReady?: (peer: Peer) => void;
  onMessage?: {
    name: string,
    callback: (message: any, messageName?: string) => void | Promise<any>,
  }[];
  allowSend: boolean = false;
  name: string = 'default';
  debug: boolean = false;
  allowedDomain: string = '';
  iFrame?: HTMLIFrameElement;

  loggy(...args: any[]) {
    if (this.debug) console.info(...args);
  }

  warny(...args: any[]) {
    if (this.debug) console.warn(...args);
  }
  
  private capitalizeFirstLetter(str: string): string {
    if (str.length === 0) {
      return str; // Return empty string if input is empty
    }
  
    const firstLetter = str.charAt(0).toUpperCase(); // Get the first letter and capitalize it
    const restOfWord = str.slice(1); // Get the remaining letters
  
    return firstLetter + restOfWord;
  }
  

  sendAgnostic(isInside: boolean, message: any, actionName?: string) {
    const here = isInside ? 'inside' : 'outside';
    const target = isInside ? window.parent : this.iFrame!.contentWindow!;
    if (!this.allowSend) {
      this.warny(`Casement: Cannot send message from ${here} window. The other side has not yet loaded, or has not yet confirmed that it is ready.`);
      return;
    }
    if (!actionName) {
      actionName = 'message';
      this.loggy('Casement: No actionName was provided. Using default actionName "message".');
    }
    // Send a message without expecting a response
    target.postMessage({ 
      type: 'casement-message',
      from: here,
      name: this.name,
      message,
      actionName,
    }, this.allowedDomain);
    this.loggy('Casement: Sent message to inside window.');
  };

  requestAgnostic(isInside: boolean, message: any, actionName?: string) {
    const here = isInside ? 'inside' : 'outside';
    const there = isInside ? 'outside' : 'inside';
    const target = isInside ? window.parent : this.iFrame!.contentWindow!;

    if (!this.allowSend) {
      this.warny(`Casement: Cannot send request from ${here} window. The other side has not yet loaded, or has not yet confirmed that it is ready.`);
      return;
    }

    // Send a message and expect a response
    return new Promise((resolve) => {
      // post a message to the parent window
      const transmissionID = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
      target.postMessage({ 
        type: `casement-request`,
        from: here,
        name: this.name,
        transmissionID, 
        message,
        actionName,
      }, this.allowedDomain);
      this.loggy(`Casement: Sent request to ${there} window, ID ${transmissionID}.`);

      // response handler
      const handleResponse = (event: MessageEvent) => {
        if (this.domainCheck(event.origin) === false) {
          this.warny(`Casement: Received response from ${there} window, but the origin (${event.origin}) did not match the allowed domain of ${this.allowedDomain}.`) 
          return;
        }
        this.loggy(`Casement: Received response from ${there} window, ID ${transmissionID}.`)
        if (event.data.type === `casement-response` &&
            event.data.transmissionID === message.transmissionID &&
            event.data.name === this.name &&
            event.data.from === there) {
          window.removeEventListener('message', handleResponse)
          resolve(event.data.message);
          this.loggy(`Casement: Accepted response from ${there} window, ID ${transmissionID}.`);
        } else {
          this.warny(`Casement: Received response from ${there} window, but something about the response did not match the request. This is likely a bug.`, event);
        }
      }
      window.addEventListener('message', handleResponse);
      this.loggy(`Casement: Listening for response from ${there} window, ID ${transmissionID}.`);
    });
  }

  on(messageName: string, handler: (message: any) => void | Promise<any>) {
    if (!this.onMessage) this.onMessage = [];
    this.onMessage.push({ name: messageName, callback: handler });
    this.loggy(`Casement: Registered handler for message "${messageName}".`);
  }

  handleIncomingAgnostic(event: MessageEvent, isInside: boolean) {
    const here = isInside ? 'inside' : 'outside';
    const there = isInside ? 'outside' : 'inside';
    const target = isInside ? window.parent : this.iFrame!.contentWindow!;

    this.loggy(`Casement: Incoming parser called.`, event);
    // Handle incoming messages. No response is needed.
    if (this.domainCheck(event.origin) === false) {
      this.warny(`Casement: Received message from ${there} window, but the origin (${event.origin}) did not match the allowed domain of ${this.allowedDomain}.`, event);
      return;
    }
    if (event.data.name !== this.name) {
      this.warny(`Casement: Received message from ${there} window, but the name (${event.data.name}) did not match the expected name of ${this.name}.`, event);
      return;
    }
    if (event.data.from !== there) {
      this.warny(`Casement: Received message appears to not come from ${there}. Not handling out of precaution.`, event);
      return;
    }

    switch (event.data.type) {
      case `casement-ready`:
        this.handleReadinessAgnostic(event, isInside);
        break;

      case `casement-message`:
        this.loggy(`Casement: Received categorized message from ${there} window.`);
        let didHandle = false;
        if (this.onMessage) this.onMessage.forEach((handler) => {
          if (handler.name === event.data.actionName || handler.name === '*') {
            this.loggy(`Casement: Accepted categorized message from ${there} window, actionName ${event.data.actionName}.`);
            handler.callback(event.data.message);
            didHandle = true;
          }
        });

        // if there's no handler, warn the user
        if (!didHandle) this.warny(`Casement: Received a message from ${there} but no handler was set for action name ${event.data.actionName}. Make sure to use the ${this.capitalizeFirstLetter(here)}.on() method to set a handler for this request.`, event);
        break;

      // Handle incoming requests, specifically. Call all callbacks.
      case `casement-request`:
        this.loggy(`Casement: Received request from ${there} window, ID ${event.data.transmissionID}.`);
        if (this.onMessage) {
          target.postMessage({ 
              type: `casement-response`, 
              message: Promise.all(this.onMessage.map((handler) => {
                if (handler.name === event.data.actionName) {
                  this.loggy(`Casement: Accepted request from ${there} window, actionName ${event.data.actionName}.`);
                  return handler.callback(event.data.message);
                }
                else if (handler.name === '*') return handler.callback(event.data.message, event.data.actionName);
              })),
              name: this.name,
              transmissionID: event.data.transmissionID,
              from: here,
            }, 
            this.allowedDomain
          );
        }
        else this.warny(`Casement: Received a request from ${there} but no handler was set. Make sure to use the ${this.capitalizeFirstLetter(here)}.on() method to set a handler for this request.`);
        break;
    }
  }

  handleReadinessAgnostic(event: MessageEvent, isInside: boolean) {
    const here = isInside ? 'inside' : 'outside';
    const there = isInside ? 'outside' : 'inside';
    const name = event.data.name;
    this.loggy(`Casement: ${here} readiness handler called.`);
    // If the message is from the inside, send a ready message back and set the allowSend flag to true.
    // If the message is from the outside, set the allowSend flag to true.
    if (this.domainCheck(event.origin) === false) {
      this.loggy(`Casement: Received ready message from ${there} window, but the origin did not match the allowed domain.`, event);
      return;
    }
    if (name !== this.name) {
      this.loggy(`Casement: Received ready message from ${here} window, but the name did not match the allowed name.`, event);
      return;
    }
    switch (event.data.from) {
      // if the message came from the inside, send a ready message back and set the allowSend flag to true
      case 'inside':
        this.loggy(`Casement: Received ready message from inside window. Sending one back...`, event);
        this.allowSend = true;
        if (this.iFrame?.contentWindow) this.iFrame.contentWindow.postMessage({ 
          type: `casement-ready`, 
          from: 'outside', 
          name: this.name 
        }, this.allowedDomain);
        if (this.onReady) this.onReady(this);
        break;

      // if the message came from the outside, set the allowSend flag to true
      case 'outside':
        if (event.data.name === this.name ) 
        this.loggy(`Casement: Received ready message from outside window. Data channel is now open and both sides can send messages.`, event);
        this.allowSend = true;
        if (this.onReady) this.onReady(this);
        break;

      default: 
        this.loggy(`Casement: Received a malformed ready message missing a 'from' property.`, event);
    }

  }

  private domainCheck(domainRaw: string) {
    if (this.allowedDomain === '*') return true;
    
    const domain = new URL(domainRaw).origin;
    if (domain === new URL(this.allowedDomain).origin) return true;
    else if (domain === window.location.origin) return true;
    else return false;
  }


}