import { Injectable } from '@angular/core';
import {
  Client,
  ClientNotificationCodec,
  WebSocketClientRWC,
  WebSocketClientRWCConfig,
  JSONClientCodec
} from '@overflow/rpc-js';
import { Subscription } from 'rxjs';
import { TypeUtil, Class, Annotation, PropertyKeyType, Method, ReflectionUtil, Decorator } from '@overflow/core-js';
import { RPCSubscriberDecoratorAttribute, RPCSubscriberDecorator } from '@overflow/commons/ui/decorator/RPCSubscriber';

export const requesterID = 'scannerUser';

export interface SubscriberMethod {
  className: PropertyKeyType;
  methodName: PropertyKeyType;
  parameterTypes: string[];

  method: Method;
  instance: any;
}

export class SubscriberParameterError extends Error {
  public constructor(message?: string) {
    super(message);
    Object.setPrototypeOf(this, new.target.prototype);
  }
}

export class SubscriberExistError extends Error {
  public constructor(message?: string) {
    super(message);
    Object.setPrototypeOf(this, new.target.prototype);
  }
}


const config: WebSocketClientRWCConfig = {
  url: 'ws://localhost:60000/scanner?requesterID=' + requesterID
};
const rwc = new WebSocketClientRWC(config);
const codec = new JSONClientCodec();

@Injectable({
  providedIn: 'root'
})
export class ProbeService extends Client {
  private notiSubscription: Subscription;

  private subscriberMap: Map<any, string[]>;
  private subscriberMethodMap: Map<string, SubscriberMethod[]>;

  public constructor() {
    super(codec, rwc);

    this.subscriberMap = new Map();
    this.subscriberMethodMap = new Map();

    this.connect();

    this.notiSubscription = this.notification()
      .subscribe((notiCodec: ClientNotificationCodec) => {
        const method = notiCodec.method();
        const params = notiCodec.params();

        const subscriberMethods: SubscriberMethod[] = this.subscriberMethodMap.get(method);
        if (undefined === subscriberMethods) {
          console.warn(`Subscriber for method[${method}] is not exist`);
          return;
        }
        subscriberMethods.forEach((subscriberMethod) => {
          try {
            const args = this.converNotificationParams(params, subscriberMethod.parameterTypes);
            subscriberMethod.method.invoke(subscriberMethod.instance, ...args);
          } catch (error) {
            console.log(error);
          }
        });
      });
    this.connectionStatus().subscribe(isConnected => {
      if (!isConnected) {
        console.log('disconnected');
      } else {
        console.log('connected');
      }
    });

  }

  /**
   * subscribeNotification
   */
  public subscribeNotification(subscriber: any) {
    if (this.subscriberMap.has(subscriber)) {
      throw new SubscriberExistError();
    }

    const type = TypeUtil.getType(subscriber);

    const clazz = Class.forType(type);
    if (undefined === clazz) {
      console.log(`Type[${subscriber.name}] is not decorated type`);
      return;
    }

    const subscriberMethodNames: string[] = [];
    this.subscriberMap.set(subscriber, subscriberMethodNames);

    const methods = clazz.getMethods();
    methods.forEach((method, propertyKey) => {
      const annon = method.getAnnotation(RPCSubscriberDecorator);
      if (undefined === annon) {
        return;
      }

      const subscriberMethodName = annon.attribute.method;
      subscriberMethodNames.push(subscriberMethodName);
      let subscriberMethods: SubscriberMethod[] = this.subscriberMethodMap.get(subscriberMethodName);
      if (undefined === subscriberMethods) {
        subscriberMethods = [];
        this.subscriberMethodMap.set(subscriberMethodName, subscriberMethods);
      }

      const paramTypes = ReflectionUtil.getParamTypeStrings(method);

      const subscriberMethod: SubscriberMethod = {
        className: clazz.getName(),
        methodName: method.getName(),
        parameterTypes: paramTypes,
        method: method,
        instance: subscriber,
      };

      subscriberMethods.push(subscriberMethod);
    });
  }

  /**
   * subscribeNotification
   */
  public unsubscribeNotification(subscriber: any) {
    if (!this.subscriberMap.has(subscriber)) {
      return;
    }

    const subscriberMethodNames = this.subscriberMap.get(subscriber);
    this.subscriberMap.delete(subscriber);
    subscriberMethodNames.forEach(methodName => {
      if (!this.subscriberMethodMap.has(methodName)) {
        return;
      }
      const subscriberMethods = this.subscriberMethodMap.get(methodName);
      subscriberMethods.some((subscriberMethod, index): boolean => {
        if (subscriberMethod.instance === subscriber) {
          delete subscriberMethods[index];
        }
        return false;
      });
    });
  }

}
