import {
  ActorModel,
} from '../models/ActorModel';
import {
  addTag,
} from '../../tags/addTag';
import {
  Epistemology,
} from '../epistemology/Epistemology';
import {
  IEpistemology,
} from '../epistemology/IEpistemology';
import {
  findAllGenerate,
} from '../querying/findAllGenerate';
import {
  FindModelArgs,
} from '../querying/FindModelArgs';
import {
  getStructuredTags,
} from '../../tags/getStructuredTags';
import {
  getTag,
} from '../../tags/getTag';
import {
  IModel,
} from '../models/IModel';
import {
  IModelConstructorArgs,
} from '../models/IModelConstructorArgs';
import {
  IOntology,
} from '../ontology/IOntology';
import {
  ISerializedWorld,
} from './ISerializedWorld';
import {
  ITag,
} from '../../tags/ITag';
import {
  IWorld,
} from './IWorld';
import {
  LocationModel,
} from '../models/LocationModel';
import {
  ModelType,
} from '../models/ModelType';
import {
  ObjectModel,
} from '../models/ObjectModel';
import {
  OnticTypes,
} from '../ontology/OnticTypes';
import {
  PortalModel,
} from '../models/PortalModel';
import {
  removeTag,
} from '../../tags/removeTag';
import {
  Tag,
} from '../../tags/Tag';
import {
  ThoughtModel,
} from '../models/ThoughtModel';
import {
  assert,
  assertValid,
} from 'ts-assertions';
import {
  TypedModelInterfaces,
} from '../models/TypedModelInterfaces';
import {
  WorldType,
} from './WorldType';

export class World implements IWorld {
  public get being() {
    return null;
  }

  private readonly __knowledge: IEpistemology<
    ModelType.Thought,
    ModelType
  > = new Epistemology(this, { modelType: ModelType.Thought });

  public get knowledge() {
    return this.__knowledge;
  }

  private __models: Readonly<
    Record<string, IModel<ModelType, OnticTypes, ModelType>>
  > = Object.freeze({});

  public get models() {
    return this.__models;
  }
  
  private readonly __name: string;
  public get name() {
    return this.__name;
  }

  private __tags: readonly ITag[] = Object.freeze([]);
  public get tags() {
    return this.__tags;
  }

  private readonly __type = WorldType;
  public get type() {
    return this.__type;
  }

  public readonly addTag = (tag: string | ITag) => (
    void (this.__tags = Object.freeze(addTag(tag, this.tags)))
  );

  public readonly removeTag = (tag: string | ITag) => (
    void (this.__tags = Object.freeze(removeTag(this.tags, tag)))
  );

  constructor(
    name: string,
    models?: Record<
      string,
      IModel<ModelType, OnticTypes, ModelType>
    >,

    initialize?: (self: IWorld) => void,
    finalize?: (self: IWorld) => void,
    tags?: Tag[] | readonly Tag[],
  ) {
    this.__name = assertValid(name);
    if (models && typeof models === 'object') {
      this.__models = models;
    }

    if (typeof initialize === 'function') {
      this.initialize = initialize;
    }

    if (typeof finalize === 'function') {
      this.finalize = finalize;
    }

    if (Array.isArray(tags)) {
      this.__tags = getStructuredTags(tags);
    }
  }

  public readonly addModel = <
    Type extends ModelType,
    Being extends OnticTypes = any,
    Knowledge extends ModelType = any,
  >(
    modelArgs: IModelConstructorArgs<
      Type,
      Being,
      Knowledge
    >,

    ctor?: new (
      world: IWorld,
      args: IModelConstructorArgs<Type, Being, Knowledge>,
    ) => IModel<Type, Being, Knowledge>,
  ): (
    Type extends keyof TypedModelInterfaces<Being, Knowledge> ?
      TypedModelInterfaces<Being, Knowledge>[Type] :
      IModel<Type, Being, Knowledge>
  ) => {
    this.validateModelArgs<Type, Being>(modelArgs);

    let temp: IModel<ModelType, Being, Knowledge>;
    if (typeof ctor === 'function') {
      temp = new ctor(this, modelArgs);
    } else if (modelArgs.type === ModelType.Actor) {
      temp = new ActorModel<Being, Knowledge>(this, {
        ...modelArgs,
        being: modelArgs.being as IOntology<ModelType.Actor, Being>,
        knowledge: modelArgs.knowledge as IEpistemology<
          ModelType.Actor,
          Knowledge
        >,

        type: modelArgs.type as ModelType.Actor,
      });
    } else if (modelArgs.type === ModelType.Location) {
      temp = new LocationModel(this, {
        ...modelArgs,
        being: modelArgs.being as IOntology<ModelType.Location, Being>,
        knowledge: null,
        type: modelArgs.type as ModelType.Location,
      }) as any;
    } else if (modelArgs.type === ModelType.Object) {
      temp = new ObjectModel(this, {
        ...modelArgs,
        being: modelArgs.being as IOntology<ModelType.Object, Being>,
        knowledge: null,
        type: modelArgs.type as ModelType.Object,
      });
    } else if (modelArgs.type === ModelType.Portal) {
      temp = new PortalModel(this, {
        ...modelArgs,
        being: modelArgs.being as IOntology<ModelType.Portal, Being>,
        knowledge: null,
        type: modelArgs.type as ModelType.Portal,
      });
    } else if (modelArgs.type === ModelType.Thought) {
      temp = new ThoughtModel(this, {
        ...modelArgs,
        being: null,
        knowledge: null,
        type: modelArgs.type as ModelType.Thought,
      });
    } else {
      throw new Error('Type argument not recognized in World.addModel.');
    }

    this.__models = Object.freeze({
      ...this.models,
      [modelArgs.name]: temp,
    });

    return temp as (
      Type extends keyof TypedModelInterfaces<Being, Knowledge> ?
        TypedModelInterfaces<Being, Knowledge>[Type] :
        IModel<Type, Being, Knowledge>
    );
  };

  public readonly getTag = (toSearch: string | ITag) => (
    getTag(this.tags, toSearch)
  );

  public readonly initialize = (self: IWorld) => {
    if (self.knowledge && typeof self.knowledge.initialize === 'function') {
      self.knowledge.initialize(self.knowledge);
    }

    this.children().forEach((child) => child.initialize(child as any));
  };

  public readonly finalize = (self: IWorld) => {
    if (self.knowledge && typeof self.knowledge.finalize === 'function') {
      self.knowledge.finalize(self.knowledge);
    }

    this.children().forEach((child) => child.finalize(child as any));
  };

  public readonly removeModel = (
    model: string | IModel<ModelType, OnticTypes, ModelType>,
  ) => {
    const copy = { ...this.__models };
    if (typeof model === 'string') {
      delete copy[model];
    } else {
      delete copy[model.name];
    }

    this.__models = Object.freeze(copy);
  };

  public readonly children = () => Object.values(this.models);

  public readonly clone = (name: string): IWorld => {
    const world: IWorld = new World(name);
    this.descendants().forEach((model) => world.addModel({ ...model }));
    return world;
  };

  public readonly descendants = () => this.findAll('*');

  public readonly destroy = () => {
    this.finalize(this);

    this.descendants().forEach((desc) => {
      desc.destroy(desc as any);
      this.removeModel(desc);
    });

    this.tags.forEach(this.removeTag);

    ((self: any) => {
      delete self.__being;
      delete self.being;
      delete self.__knowledge;
      delete self.knowledge;
      delete self.__models;
      delete self.models;
      delete self.__tags;
      delete self.tags;
      delete self.__type;
      delete self.type;
    })(this);
  };

  public readonly find = <
    Type extends ModelType,
    Being extends OnticTypes,
    Knowledge extends ModelType,
  >(
    args: string | FindModelArgs<Type, Being, Knowledge>,
  ) => this.findAllGenerator(
    typeof args === 'string' ?
      { name: args } :
      args,
  ).next().value || null;

  public readonly findAll = <
    Type extends ModelType,
    Being extends OnticTypes,
    Knowledge extends ModelType,
  >(
    args: '*' | FindModelArgs<Type, Being, Knowledge>,
  ): readonly IModel<Type, Being, Knowledge>[] => {
    const ret: IModel<Type, Being, Knowledge>[] = [];
    const gen = this.findAllGenerator(args);
    while (true) {
      const {
        done,
        value,
      } = gen.next();

      if (done || !value) {
        break;
      }

      if (!value) {
        break;
      }

      ret.push(value as IModel<Type, Being, Knowledge>);
    }

    return Object.freeze(ret);
  };

  public readonly findAllGenerator = ((self: IWorld) => (
    function* (args: '*' | FindModelArgs<ModelType, OnticTypes, ModelType>) {
      assert(args, 'The args argument to World.findAllGenerator was not valid.');
      yield* findAllGenerate(Object.values(self.models), args);
    }
  ))(this);

  public readonly validateModelArgs = <
    Type extends ModelType,
    Being extends OnticTypes,
  >(args: any): args is IModelConstructorArgs<Type, Being> => {
    assert(args, 'There were no arguments passed to World.addModel.');

    assert(
      args.name,
      'There was no name property in the argument array passed to ' +
        'World.addModel.',
    );

    assert(
      !(args.name in this.models),
      'There was already a model with the provided name in the models map.', 
    )

    assert(
      args.type,
      'There was no type property in the arguments array passed to ' +
        'World.addModel.',
    );

    return true;
  };

  public readonly serialize = (
    self: IWorld,
    spaces: number = 0,
  ) => JSON.stringify(self.serializeToObject(self), null, spaces);

  public readonly serializeToObject = (
    self: IWorld,
  ): ISerializedWorld => ({
    knowledge: self.knowledge.serializeToObject(self.knowledge),
    name: self.name,
    models: Object.values(self.models).map((model) => model.serializeToObject(model)),
    tags: [ ...self.tags ],
  });
}
