import { Injectable, Inject } from '@angular/core';
import { Store, select } from '@ngrx/store';

import {
  Type,
  PropertyKeyType,
} from '@loafer/core';

import { TypeUtil } from '@loafer/core/util/TypeUtil';

import {
  Class,
  Method,
  Metadata,
} from '@loafer/core/reflect';

import { RPCSubscriberDecorator } from '../decorator';

import {
  RPCClientNotificationCodec,
} from '../protocol';
import { SubscriberParameterError } from '../core';

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

  method: Method;
  instance: any;
}

@Injectable()
export class RPCSubscribeService {
  private subscriberTypes: Set<Type<any>>;
  private subscribers: Set<any>;
  private subscriberMethodMap: Map<string, SubscriberMethod[]>;

  constructor(
  ) {
    this.subscriberTypes = new Set();
    this.subscribers = new Set();
    this.subscriberMethodMap = new Map();
  }

  public addSubscriber(subscriber: Type<any>): void {
    const type = TypeUtil.getType(subscriber);

    if (this.subscriberTypes.has(type)) {
      // console.log(`Subscriber[${type.name}] has been added`);
      return;
    }

    this.subscriberTypes.add(type);

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

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

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

      const paramTypes = this.getParamTypes(method);

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

      subscriberMethods.push(subscriberMethod);
    });
  }

  public notify(codec: RPCClientNotificationCodec): void {
    const method = codec.method();
    const params = codec.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.converParams(params, subscriberMethod.parameterTypes);
        subscriberMethod.method.invoke(subscriberMethod.instance, ...args);
      } catch (error) {
        console.error(error);
      }
    });
  }

  private getParamTypes(method: Method): string[] {
    if (undefined === method || null === method || 0 === method.getParameterCount()) {
      return [];
    }

    const parameters = method.getParameters();
    const results: string[] = [];
    for (let indexI = 0; indexI < parameters.length; indexI++) {
      const paramType = parameters[indexI].getType();
      results.push(paramType.name);
    }
    return results;
  }

  private converParams(params: string[], paramTypes: string[]): any[] {
    const results: any[] = [];

    if (undefined === params || null === params || 0 === params.length) {
      return results;
    }
    if (undefined === paramTypes || null === paramTypes || 0 === paramTypes.length) {
      return results;
    }
    if (params.length !== paramTypes.length) {
      throw new SubscriberParameterError(`Count is not same from server[${params.length}] and method[${paramTypes.length}]`);
    }
    for (let indexI = 0; indexI < params.length; indexI++) {
      const param = params[indexI];
      const type = paramTypes[indexI];
      switch (type) {
        case 'Object':
        case 'Array':
        case 'Map':
          results.push(JSON.parse(param));
          break;
        case 'String':
          results.push(param);
          break;
        case 'Number':
          results.push(Number(param));
          break;
        case 'Boolean':
          results.push(Boolean(param));
          break;
        case 'Function':
          throw new SubscriberParameterError(`Function type [${indexI}] is not allowed`);
        default:
          throw new SubscriberParameterError(`${type} type parameter[${indexI}] is not allowed`);
      }
    }
    return results;
  }
}
