import { IExpressionParser } from '@aurelia/expression-parser';
import { Protocol, camelCase, emptyArray, firstDefined, getResourceKeyFor, mergeArrays, resolve, resource, resourceBaseName, isString } from '@aurelia/kernel';
import { defineMetadata, getMetadata } from './utilities-metadata';
import { IAttrMapper } from './attribute-mapper';
import {
  AttributeBindingInstruction,
  IteratorBindingInstruction,
  ListenerBindingInstruction,
  MultiAttrInstruction,
  PropertyBindingInstruction,
  RefBindingInstruction,
  SpreadValueBindingInstruction,
} from './instructions';
import { aliasRegistration, etIsFunction, etIsProperty, tcObjectFreeze, singletonRegistration } from './utilities';

import type {
  Constructable,
  IContainer,
  IServiceLocator,
  PartialResourceDefinition,
  ResourceDefinition,
  ResourceType,
  StaticResourceType,
} from '@aurelia/kernel';
import { AttrSyntax, IAttributeParser } from './attribute-pattern';
import { ErrorNames, createMappedError } from './errors';
import { IAttributeComponentDefinition, IElementComponentDefinition, IComponentBindablePropDefinition } from './interfaces-template-compiler';
import type { IInstruction } from './instructions';
import { BindingMode, InternalBindingMode } from './binding-mode';

export type PartialBindingCommandDefinition = PartialResourceDefinition;
export type BindingCommandStaticAuDefinition = PartialBindingCommandDefinition & {
  type: 'binding-command';
};

export interface IPlainAttrCommandInfo {
  readonly node: Element;
  readonly attr: AttrSyntax;
  readonly bindable: null;
  readonly def: null;
}

export interface IBindableCommandInfo {
  readonly node: Element;
  readonly attr: AttrSyntax;
  readonly bindable: IComponentBindablePropDefinition;
  readonly def: IAttributeComponentDefinition | IElementComponentDefinition;
}

export type ICommandBuildInfo = IPlainAttrCommandInfo | IBindableCommandInfo;

export type BindingCommandInstance<T extends {} = {}> = {
  /**
   * Characteristics of a binding command.
   * - `false`: The normal process (check custom attribute -> check bindable -> command.build()) should take place.
   * - `true`: The binding command wants to take over the processing of an attribute. The template compiler keeps the attribute as is in compilation, instead of executing the normal process.
   */
  ignoreAttr: boolean;
  build(info: ICommandBuildInfo, parser: IExpressionParser, mapper: IAttrMapper): IInstruction;
} & T;

export type BindingCommandType<T extends Constructable = Constructable> = ResourceType<T, BindingCommandInstance, PartialBindingCommandDefinition>;
export type BindingCommandKind = {
  readonly name: string;
  keyFrom(name: string): string;
  // isType<T>(value: T): value is (T extends Constructable ? BindingCommandType<T> : never);
  define<T extends Constructable>(name: string, Type: T): BindingCommandType<T>;
  define<T extends Constructable>(def: PartialBindingCommandDefinition, Type: T): BindingCommandType<T>;
  define<T extends Constructable>(nameOrDef: string | PartialBindingCommandDefinition, Type: T): BindingCommandType<T>;
  getAnnotation<K extends keyof PartialBindingCommandDefinition>(Type: Constructable, prop: K): PartialBindingCommandDefinition[K] | undefined;
  find(container: IContainer, name: string): BindingCommandDefinition | null;
  get(container: IServiceLocator, name: string): BindingCommandInstance;
};

export type BindingCommandDecorator = <T extends Constructable>(Type: T, context: ClassDecoratorContext) => BindingCommandType<T>;

/**
 * Decorator to describe a class as a binding command resource
 */
export function bindingCommand(name: string): BindingCommandDecorator;
export function bindingCommand(definition: PartialBindingCommandDefinition): BindingCommandDecorator;
export function bindingCommand(nameOrDefinition: string | PartialBindingCommandDefinition): BindingCommandDecorator {
  return function <T extends Constructable>(target: T, context: ClassDecoratorContext): BindingCommandType<T> {
    context.addInitializer(function (this) {
      BindingCommand.define(nameOrDefinition, target);
    });
    return target as BindingCommandType<T>;
  };
}

export class BindingCommandDefinition<T extends Constructable = Constructable> implements ResourceDefinition<T, BindingCommandInstance> {
  private constructor(
    public readonly Type: BindingCommandType<T>,
    public readonly name: string,
    public readonly aliases: readonly string[],
    public readonly key: string,
  ) {}

  public static create<T extends Constructable = Constructable>(
    nameOrDef: string | PartialBindingCommandDefinition,
    Type: BindingCommandType<T>,
  ): BindingCommandDefinition<T> {

    let name: string;
    let def: PartialBindingCommandDefinition;
    if (isString(nameOrDef)) {
      name = nameOrDef;
      def = { name };
    } else {
      name = nameOrDef.name;
      def = nameOrDef;
    }

    return new BindingCommandDefinition(
      Type,
      firstDefined(getCommandAnnotation(Type, 'name'), name),
      mergeArrays(getCommandAnnotation(Type, 'aliases'), def.aliases, Type.aliases),
      getCommandKeyFrom(name),
    );
  }

  public register(container: IContainer, aliasName?: string | undefined): void {
    const $Type = this.Type;
    const key = typeof aliasName === 'string' ? getCommandKeyFrom(aliasName) : this.key;
    const aliases = this.aliases;

    if (!container.has(key, false)) {
      container.register(
        container.has($Type, false) ? null : singletonRegistration($Type, $Type),
        aliasRegistration($Type, key),
        ...aliases.map(alias => aliasRegistration($Type, getCommandKeyFrom(alias))),
      );
    } /* istanbul ignore next */ else if (__DEV__) {
      // eslint-disable-next-line no-console
      console.warn(`[DEV:aurelia] ${createMappedError(ErrorNames.binding_command_existed, this.name)}`);
    }
  }
}

const bindingCommandTypeName = 'binding-command';
const cmdBaseName = /*@__PURE__*/getResourceKeyFor(bindingCommandTypeName);
const getCommandKeyFrom = (name: string): string => `${cmdBaseName}:${name}`;
const getCommandAnnotation = <K extends keyof PartialBindingCommandDefinition>(
  Type: Constructable,
  prop: K,
): PartialBindingCommandDefinition[K] | undefined =>
  getMetadata<PartialBindingCommandDefinition[K]>(Protocol.annotation.keyFor(prop), Type);

export const BindingCommand = /*@__PURE__*/ (() => {

  const staticResourceDefinitionMetadataKey = '__au_static_resource__';
  const getDefinitionFromStaticAu = <Def extends ResourceDefinition, C extends Constructable = Constructable>(
    // eslint-disable-next-line @typescript-eslint/ban-types
    Type: C | Function,
    typeName: string,
    createDef: (au: PartialResourceDefinition<Def>, Type: C) => Def,
  ): Def => {
    let def = getMetadata(staticResourceDefinitionMetadataKey, Type) as Def;
    if (def == null) {
      if ((Type as StaticResourceType<Def>).$au?.type === typeName) {
        def = createDef((Type as StaticResourceType<Def>).$au!, Type as C);
        defineMetadata(def, Type, staticResourceDefinitionMetadataKey);
      }
    }
    return def;
  };

  return tcObjectFreeze<BindingCommandKind>({
    name: cmdBaseName,
    keyFrom: getCommandKeyFrom,
    // isType<T>(value: T): value is (T extends Constructable ? BindingCommandType<T> : never) {
    //   return isFunction(value) && hasOwnMetadata(cmdBaseName, value);
    // },
    define<T extends Constructable<BindingCommandInstance>>(nameOrDef: string | PartialBindingCommandDefinition, Type: T): T & BindingCommandType<T> {
      const definition = BindingCommandDefinition.create(nameOrDef, Type as Constructable<BindingCommandInstance>);
      const $Type = definition.Type as BindingCommandType<T>;

      // registration of resource name is a requirement for the resource system in kernel (module-loader)
      defineMetadata(definition, $Type, cmdBaseName, resourceBaseName);

      return $Type;
    },
    getAnnotation: getCommandAnnotation,
    find(container, name) {
      const Type = container.find<BindingCommandType>(bindingCommandTypeName, name);
      return Type == null
        ? null
        : getMetadata<BindingCommandDefinition>(cmdBaseName, Type) ?? getDefinitionFromStaticAu<BindingCommandDefinition, BindingCommandType>(Type, bindingCommandTypeName, BindingCommandDefinition.create) ?? null;
    },
    get(container, name) {
      if (__DEV__) {
        try {
          return container.get<BindingCommandInstance>(resource(getCommandKeyFrom(name)));
        } catch (ex) {
          // eslint-disable-next-line no-console
          console.log(`\n\n\n[DEV:aurelia] Cannot retrieve binding command with name\n\n\n\n\n`, name);
          throw ex;
        }
      }
      return container.get<BindingCommandInstance>(resource(getCommandKeyFrom(name)));
    },
  });
})();

export class OneTimeBindingCommand implements BindingCommandInstance {
  public static readonly $au: BindingCommandStaticAuDefinition = {
    type: bindingCommandTypeName,
    name: 'one-time',
  };

  public get ignoreAttr() { return false; }

  public build(info: ICommandBuildInfo, exprParser: IExpressionParser, attrMapper: IAttrMapper): PropertyBindingInstruction {
    const attr = info.attr;
    let target = attr.target;
    let value = info.attr.rawValue;
    value = value === '' ? camelCase(target) : value;
    if (info.bindable == null) {
      target = attrMapper.map(info.node, target)
        // if the mapper doesn't know how to map it
        // use the default behavior, which is camel-casing
        ?? camelCase(target);
    } else {
      target = info.bindable.name;
    }
    return new PropertyBindingInstruction(exprParser.parse(value, etIsProperty), target, InternalBindingMode.oneTime);
  }
}

export class ToViewBindingCommand implements BindingCommandInstance {
  public static readonly $au: BindingCommandStaticAuDefinition = {
    type: bindingCommandTypeName,
    name: 'to-view',
  };
  public get ignoreAttr() { return false; }

  public build(info: ICommandBuildInfo, exprParser: IExpressionParser, attrMapper: IAttrMapper): PropertyBindingInstruction {
    const attr = info.attr;
    let target = attr.target;
    let value = info.attr.rawValue;
    value = value === '' ? camelCase(target) : value;
    if (info.bindable == null) {
      target = attrMapper.map(info.node, target)
        // if the mapper doesn't know how to map it
        // use the default behavior, which is camel-casing
        ?? camelCase(target);
    } else {
      target = info.bindable.name;
    }
    return new PropertyBindingInstruction(exprParser.parse(value, etIsProperty), target, InternalBindingMode.toView);
  }
}

export class FromViewBindingCommand implements BindingCommandInstance {
  public static readonly $au: BindingCommandStaticAuDefinition = {
    type: bindingCommandTypeName,
    name: 'from-view',
  };
  public get ignoreAttr() { return false; }

  public build(info: ICommandBuildInfo, exprParser: IExpressionParser, attrMapper: IAttrMapper): PropertyBindingInstruction {
    const attr = info.attr;
    let target = attr.target;
    let value = attr.rawValue;
    value = value === '' ? camelCase(target) : value;
    if (info.bindable == null) {
      target = attrMapper.map(info.node, target)
        // if the mapper doesn't know how to map it
        // use the default behavior, which is camel-casing
        ?? camelCase(target);
    } else {
      target = info.bindable.name;
    }
    return new PropertyBindingInstruction(exprParser.parse(value, etIsProperty), target, InternalBindingMode.fromView);
  }
}

export class TwoWayBindingCommand implements BindingCommandInstance {
  public static readonly $au: BindingCommandStaticAuDefinition = {
    type: bindingCommandTypeName,
    name: 'two-way',
  };
  public get ignoreAttr() { return false; }

  public build(info: ICommandBuildInfo, exprParser: IExpressionParser, attrMapper: IAttrMapper): PropertyBindingInstruction {
    const attr = info.attr;
    let target = attr.target;
    let value = attr.rawValue;
    value = value === '' ? camelCase(target) : value;
    if (info.bindable == null) {
      target = attrMapper.map(info.node, target)
        // if the mapper doesn't know how to map it
        // use the default behavior, which is camel-casing
        ?? camelCase(target);
    } else {
      target = info.bindable.name;
    }
    return new PropertyBindingInstruction(exprParser.parse(value, etIsProperty), target, InternalBindingMode.twoWay);
  }
}

export class DefaultBindingCommand implements BindingCommandInstance {
  public static readonly $au: BindingCommandStaticAuDefinition = {
    type: bindingCommandTypeName,
    name: 'bind',
  };
  public get ignoreAttr() { return false; }

  public build(info: ICommandBuildInfo, exprParser: IExpressionParser, attrMapper: IAttrMapper): PropertyBindingInstruction {
    const attr = info.attr;
    const bindable = info.bindable;
    let value = attr.rawValue;
    let target = attr.target;
    let defDefaultMode: string | number;
    let mode: string | number;
    value = value === '' ? camelCase(target) : value;
    if (bindable == null) {
      mode = attrMapper.isTwoWay(info.node, target) ? InternalBindingMode.twoWay : InternalBindingMode.toView;
      target = attrMapper.map(info.node, target)
        // if the mapper doesn't know how to map it
        // use the default behavior, which is camel-casing
        ?? camelCase(target);
    } else {
      defDefaultMode = (info.def as IAttributeComponentDefinition).defaultBindingMode ?? 0;
      mode = bindable.mode === 0 || bindable.mode == null
        ? defDefaultMode == null || defDefaultMode === 0
          ? InternalBindingMode.toView
          : defDefaultMode
        : bindable.mode;
      target = bindable.name;
    }
    return new PropertyBindingInstruction(
      exprParser.parse(value, etIsProperty),
      target,
      isString(mode)
        ? BindingMode[mode as keyof typeof BindingMode] ?? InternalBindingMode.default
        : mode as BindingMode
    );
  }
}

export class ForBindingCommand implements BindingCommandInstance {
  public static readonly $au: BindingCommandStaticAuDefinition = {
    type: bindingCommandTypeName,
    name: 'for',
  };

  public get ignoreAttr() { return false; }

  /** @internal */
  private readonly _attrParser = resolve(IAttributeParser);

  public build(info: ICommandBuildInfo, exprParser: IExpressionParser): IInstruction {
    const target = info.bindable === null
      ? camelCase(info.attr.target)
      : info.bindable.name;
    const forOf = exprParser.parse(info.attr.rawValue, 'IsIterator');
    let props: MultiAttrInstruction[] = emptyArray;
    if (forOf.semiIdx > -1) {
      const attr = info.attr.rawValue.slice(forOf.semiIdx + 1);
      const i = attr.indexOf(':');
      if (i > -1) {
        const attrName = attr.slice(0, i).trim();
        const attrValue = attr.slice(i + 1).trim();
        const attrSyntax = this._attrParser.parse(attrName, attrValue);
        props = [new MultiAttrInstruction(attrValue, attrSyntax.target, attrSyntax.command)];
      }
    }
    return new IteratorBindingInstruction(forOf, target, props);
  }
}

export class TriggerBindingCommand implements BindingCommandInstance {
  public static readonly $au: BindingCommandStaticAuDefinition = {
    type: bindingCommandTypeName,
    name: 'trigger',
  };
  public get ignoreAttr() { return true; }

  public build(info: ICommandBuildInfo, exprParser: IExpressionParser): IInstruction {
    return new ListenerBindingInstruction(
      exprParser.parse(info.attr.rawValue, etIsFunction),
      info.attr.target,
      false,
      info.attr.parts?.[2] ?? null
    );
  }
}

export class CaptureBindingCommand implements BindingCommandInstance {
  public static readonly $au: BindingCommandStaticAuDefinition = {
    type: bindingCommandTypeName,
    name: 'capture',
  };
  public get ignoreAttr() { return true; }

  public build(info: ICommandBuildInfo, exprParser: IExpressionParser): IInstruction {
    return new ListenerBindingInstruction(
      exprParser.parse(info.attr.rawValue, etIsFunction),
      info.attr.target,
      true,
      info.attr.parts?.[2] ?? null
    );
  }
}

/**
 * Attr binding command. Compile attr with binding symbol with command `attr` to `AttributeBindingInstruction`
 */
export class AttrBindingCommand implements BindingCommandInstance {
  public static readonly $au: BindingCommandStaticAuDefinition = {
    type: bindingCommandTypeName,
    name: 'attr',
  };
  public get ignoreAttr() { return true; }

  public build(info: ICommandBuildInfo, exprParser: IExpressionParser): IInstruction {
    const attr = info.attr;
    const target = attr.target;
    let value = attr.rawValue;
    value = value === '' ? camelCase(target) : value;
    return new AttributeBindingInstruction(target, exprParser.parse(value, etIsProperty), target);
  }
}

/**
 * Style binding command. Compile attr with binding symbol with command `style` to `AttributeBindingInstruction`
 */
export class StyleBindingCommand implements BindingCommandInstance {
  public static readonly $au: BindingCommandStaticAuDefinition = {
    type: bindingCommandTypeName,
    name: 'style',
  };
  public get ignoreAttr() { return true; }

  public build(info: ICommandBuildInfo, exprParser: IExpressionParser): IInstruction {
    return new AttributeBindingInstruction('style', exprParser.parse(info.attr.rawValue, etIsProperty), info.attr.target);
  }
}

/**
 * Class binding command. Compile attr with binding symbol with command `class` to `AttributeBindingInstruction`
 */
export class ClassBindingCommand implements BindingCommandInstance {
  public static readonly $au: BindingCommandStaticAuDefinition = {
    type: bindingCommandTypeName,
    name: 'class',
  };
  public get ignoreAttr() { return true; }

  public build(info: ICommandBuildInfo, exprParser: IExpressionParser): IInstruction {
    return new AttributeBindingInstruction('class', exprParser.parse(info.attr.rawValue, etIsProperty), info.attr.target);
  }
}

/**
 * Binding command to refer different targets (element, custom element/attribute view models, controller) attached to an element
 */
export class RefBindingCommand implements BindingCommandInstance {
  public static readonly $au: BindingCommandStaticAuDefinition = {
    type: bindingCommandTypeName,
    name: 'ref',
  };
  public get ignoreAttr() { return true; }

  public build(info: ICommandBuildInfo, exprParser: IExpressionParser): IInstruction {
    return new RefBindingInstruction(exprParser.parse(info.attr.rawValue, etIsProperty), info.attr.target);
  }
}

export class SpreadValueBindingCommand implements BindingCommandInstance {
  public static readonly $au: BindingCommandStaticAuDefinition = {
    type: bindingCommandTypeName,
    name: 'spread',
  };
  public get ignoreAttr() { return false; }

  public build(info: ICommandBuildInfo): IInstruction {
    return new SpreadValueBindingInstruction(info.attr.target as '$bindables' | '$element', info.attr.rawValue);
  }
}
