import * as TypeORM from "typeorm";
import * as LRUCache from "lru-cache";
import * as DeepCopy from "deepcopy";

declare var syzoj: any;

interface Paginater {
  pageCnt: number;
  perPage: number;
  currPage: number;
}

enum PaginationType {
  PREV = -1,
  NEXT = 1
}

enum PaginationIDOrder {
  ASC = 1,
  DESC = -1
}

const caches: Map<string, LRUCache<number, Model>> = new Map();

function ensureCache(modelName) {
  if (!caches.has(modelName)) {
    caches.set(modelName, new LRUCache({
      max: syzoj.config.db.cache_size
    }));
  }

  return caches.get(modelName);
}

function cacheGet(modelName, id) {
  return ensureCache(modelName).get(parseInt(id));
}

function cacheSet(modelName, id, data) {
  if (data == null) {
    ensureCache(modelName).del(id);
  } else {
    ensureCache(modelName).set(parseInt(id), data);
  }
}

export default class Model extends TypeORM.BaseEntity {
  static cache = false;

  static async findById<T extends TypeORM.BaseEntity>(this: TypeORM.ObjectType<T>, id?: number): Promise<T | undefined> {
    const doQuery = async () => await (this as any).findOne(parseInt(id as any) || 0);

    if ((this as typeof Model).cache) {
      const resultObject = cacheGet(this.name, id);
      if (resultObject) {
        return (this as typeof Model).create(resultObject) as any as T;
      }

      const result = await doQuery();
      if (result) {
        cacheSet(this.name, id, result.toPlain());
      }
      return result;
    } else {
      return await doQuery();
    }
  }

  toPlain() {
    const object = {};
    TypeORM.getConnection().getMetadata(this.constructor).ownColumns.map(column => column.propertyName).forEach(key => {
      object[key] = DeepCopy(this[key]);
    });
    return object;
  }

  async destroy() {
    const id = (this as any).id;
    await TypeORM.getManager().remove(this);
    await (this.constructor as typeof Model).deleteFromCache(id);
  }

  static async deleteFromCache(id) {
    if (this.cache) {
      cacheSet(this.name, id, null);
    }
  }

  async save(): Promise<this> {
    await super.save();
    if ((this.constructor as typeof Model).cache) {
      cacheSet(this.constructor.name, (this as any).id, this);
    }
    return this;
  }

  static async countQuery<T extends TypeORM.BaseEntity>(this: TypeORM.ObjectType<T>, query: TypeORM.SelectQueryBuilder<T> | string) {
    let parameters: any[] = null;
    if (typeof query !== 'string') {
      [query, parameters] = query.getQueryAndParameters();
    }

    return parseInt((
      await TypeORM.getManager().query(`SELECT COUNT(*) FROM (${query}) AS \`__tmp_table\``, parameters)
    )[0]['COUNT(*)']);
  }

  static async countForPagination(where) {
    const queryBuilder = where instanceof TypeORM.SelectQueryBuilder
                       ? where
                       : this.createQueryBuilder().where(where);
    return await queryBuilder.getCount();
  }

  static async queryAll(queryBuilder) {
    return await queryBuilder.getMany();
  }

  static async queryPage(paginater: Paginater, where, order, largeData = false) {
    if (!paginater.pageCnt) return [];

    const queryBuilder = where instanceof TypeORM.SelectQueryBuilder
                       ? where
                       : this.createQueryBuilder().where(where);

    if (order) queryBuilder.orderBy(order);

    queryBuilder.skip((paginater.currPage - 1) * paginater.perPage)
                .take(paginater.perPage);

    if (largeData) {
      const rawResult = await queryBuilder.select('id').getRawMany();
      return await Promise.all(rawResult.map(async result => this.findById(result.id)));
    }

    return queryBuilder.getMany();
  }

  static async queryPageWithLargeData<T extends TypeORM.BaseEntity>(this: TypeORM.ObjectType<T>,
                                                                    queryBuilder: TypeORM.SelectQueryBuilder<T>,
                                                                    { currPageTop, currPageBottom, perPage },
                                                                    idOrder: PaginationIDOrder,
                                                                    pageType: PaginationType) {
    const queryBuilderBak = queryBuilder.clone();

    const result = {
      meta: {
        hasPrevPage: false,
        hasNextPage: false,
        top: 0,
        bottom: 0
      },
      data: []
    };

    queryBuilder.take(perPage);
    if (pageType === PaginationType.PREV) {
      if (currPageTop != null) {
        queryBuilder.andWhere(`id ${idOrder === PaginationIDOrder.DESC ? '>' : '<'} :currPageTop`, { currPageTop });
        queryBuilder.orderBy('id', idOrder === PaginationIDOrder.DESC ? 'ASC' : 'DESC');
      }
    } else if (pageType === PaginationType.NEXT) {
      if (currPageBottom != null) {
        queryBuilder.andWhere(`id ${idOrder === PaginationIDOrder.DESC ? '<' : '>'} :currPageBottom`, { currPageBottom });
        queryBuilder.orderBy('id', idOrder === PaginationIDOrder.DESC ? 'DESC' : 'ASC');
      }
    } else queryBuilder.orderBy('id', idOrder === PaginationIDOrder.DESC ? 'DESC' : 'ASC');

    result.data = await queryBuilder.getMany();
    result.data.sort((a, b) => (a.id - b.id) * idOrder);

    if (result.data.length === 0) return result;

    const queryBuilderHasPrev = queryBuilderBak.clone(),
          queryBuilderHasNext = queryBuilderBak;

    result.meta.top = result.data[0].id;
    result.meta.bottom = result.data[result.data.length - 1].id;

    // Run two queries in parallel.
    await Promise.all(([
      async () => result.meta.hasPrevPage = !!(await queryBuilderHasPrev.andWhere(`id ${idOrder === PaginationIDOrder.DESC ? '>' : '<'} :id`, {
                                                                          id: result.meta.top
                                                                        }).take(1).getOne()),
      async () => result.meta.hasNextPage = !!(await queryBuilderHasNext.andWhere(`id ${idOrder === PaginationIDOrder.DESC ? '<' : '>'} :id`, {
                                                                          id: result.meta.bottom
                                                                        }).take(1).getOne())
    ]).map(f => f()));

    return result;
  }

  static async queryRange(range: any[], where, order) {
    range[0] = parseInt(range[0]);
    range[1] = parseInt(range[1]);

    const queryBuilder = where instanceof TypeORM.SelectQueryBuilder
                       ? where
                       : this.createQueryBuilder().where(where);

    if (order) queryBuilder.orderBy(order);

    queryBuilder.skip(range[0] - 1)
                .take(range[1] - range[0] + 1);

    return queryBuilder.getMany();
  }
}