import { Types } from '../typeSystem/types';
import { IType } from '../typeSystem/itype';
import { Type } from '../typeSystem/type';
import { Operators, Operator } from '../ast/operators';
import { MultiType } from '../typeSystem/multiType';
import { Config } from '../util/config';

function buildInfixAddTable (): IType[][] {
  const table: IType[][] = [[], [], [], [], []]

  table[Types.INTEGER.ord][Types.INTEGER.ord] = Types.INTEGER;
  table[Types.INTEGER.ord][Types.REAL.ord] = Types.REAL;
  table[Types.INTEGER.ord][Types.STRING.ord] = Types.STRING;

  table[Types.REAL.ord][Types.INTEGER.ord] = Types.REAL;
  table[Types.REAL.ord][Types.REAL.ord] = Types.REAL;
  table[Types.REAL.ord][Types.STRING.ord] = Types.STRING;

  table[Types.STRING.ord][Types.INTEGER.ord] = Types.STRING;
  table[Types.STRING.ord][Types.REAL.ord] = Types.STRING;
  table[Types.STRING.ord][Types.STRING.ord] = Types.STRING;
  table[Types.STRING.ord][Types.BOOLEAN.ord] = Types.STRING;
  table[Types.STRING.ord][Types.CHAR.ord] = Types.STRING;

  table[Types.CHAR.ord][Types.CHAR.ord] = Types.STRING;
  table[Types.CHAR.ord][Types.STRING.ord] = Types.STRING;

  return table;
}

function buildInfixMultiDivSubTable (): IType[][] {
  const table: IType[][] = [[], []];

  table[Types.INTEGER.ord][Types.INTEGER.ord] = Types.INTEGER;
  table[Types.INTEGER.ord][Types.REAL.ord] = Types.REAL;

  table[Types.REAL.ord][Types.INTEGER.ord] = Types.REAL;
  table[Types.REAL.ord][Types.REAL.ord] = Types.REAL;

  return table;
}

function buildInfixEqualityInequalityTable (): IType[][] {
  const table: IType[][] = [[], [], [], [], []];

  table[Types.INTEGER.ord][Types.INTEGER.ord] = Types.BOOLEAN;

  table[Types.REAL.ord][Types.REAL.ord] = Types.BOOLEAN;

  table[Types.BOOLEAN.ord][Types.BOOLEAN.ord] = Types.BOOLEAN;

  table[Types.STRING.ord][Types.STRING.ord] = Types.BOOLEAN;

  table[Types.CHAR.ord][Types.CHAR.ord] = Types.BOOLEAN;

  return table;
}

function buildInfixRelationalTable (): IType[][] {
  const table: IType[][] = [[], [], [], [], []];

  table[Types.INTEGER.ord][Types.INTEGER.ord] = Types.BOOLEAN;

  table[Types.REAL.ord][Types.REAL.ord] = Types.BOOLEAN;

  table[Types.STRING.ord][Types.STRING.ord] = Types.BOOLEAN;

  table[Types.CHAR.ord][Types.CHAR.ord] = Types.BOOLEAN;

  return table;
}

function buildInfixAndOrTable (): IType[][] {
  const table: IType[][] = [[], [], [], []];

  table[Types.BOOLEAN.ord][Types.BOOLEAN.ord] = Types.BOOLEAN;

  return table;
}

function buildInfixModTable (): IType[][] {
  const table: IType[][] = [[]];

  table[Types.INTEGER.ord][Types.INTEGER.ord] = Types.INTEGER;

  return table;
}

function buildUnarySumSubList (): IType[] {
  const list: IType[] = [];

  list[Types.INTEGER.ord] = Types.INTEGER;

  list[Types.REAL.ord] = Types.REAL;

  return list;
}

function buildUnaryNegList (): IType[] {
  const list: IType[] = [];

  list[Types.BOOLEAN.ord] = Types.BOOLEAN;

  return list;
}

function buildInfixCompatibilityTable (): WeakMap<Operator, IType[][]> {
  const compatibilityMap = new WeakMap();
  compatibilityMap.set(Operators.ADD, buildInfixAddTable());
  compatibilityMap.set(Operators.SUB, buildInfixMultiDivSubTable());
  compatibilityMap.set(Operators.MULT, buildInfixMultiDivSubTable());
  compatibilityMap.set(Operators.DIV, buildInfixMultiDivSubTable());
  compatibilityMap.set(Operators.EQ, buildInfixEqualityInequalityTable());
  compatibilityMap.set(Operators.NEQ, buildInfixEqualityInequalityTable());
  compatibilityMap.set(Operators.GE, buildInfixRelationalTable());
  compatibilityMap.set(Operators.GT, buildInfixRelationalTable());
  compatibilityMap.set(Operators.LE, buildInfixRelationalTable());
  compatibilityMap.set(Operators.LT, buildInfixRelationalTable());
  compatibilityMap.set(Operators.OR, buildInfixAndOrTable());
  compatibilityMap.set(Operators.AND, buildInfixAndOrTable());
  compatibilityMap.set(Operators.MOD, buildInfixModTable());
  return compatibilityMap;
}

function buildUnaryCompatibilityTable (): WeakMap<Operator, IType[]> {
  const compatibilityMap = new WeakMap();
  compatibilityMap.set(Operators.ADD, buildUnarySumSubList());
  compatibilityMap.set(Operators.SUB, buildUnarySumSubList());
  compatibilityMap.set(Operators.NOT, buildUnaryNegList());
  return compatibilityMap;
}

const infixMap = buildInfixCompatibilityTable();
const unaryMap = buildUnaryCompatibilityTable();

export function resultTypeAfterInfixOp (
  operator: Operator,
  leftExpressionType: IType,
  rightExpressionType: IType
): IType {
  try {
    if (leftExpressionType instanceof MultiType && rightExpressionType instanceof MultiType) {
      let newMulti = [];
      for (let i = 0; i < leftExpressionType.types.length; ++i) {
        const typeA = leftExpressionType.types[i];
        for (let j = 0; j < rightExpressionType.types.length; ++i) {
          const typeB = rightExpressionType.types[j];
          newMulti.push(resultTypeAfterInfixOp(operator, typeA, typeB));
        }
      }
      newMulti = newMulti.filter(x => !x.isCompatible(Types.UNDEFINED));
      if (newMulti.length <= 0) {
        if (Config.enable_type_casting) {
          if (leftExpressionType.isCompatible(Types.INTEGER) || leftExpressionType.isCompatible(Types.REAL)) {
            if (rightExpressionType.isCompatible(Types.INTEGER) || rightExpressionType.isCompatible(Types.REAL)) {
              return new MultiType([Types.INTEGER, Types.REAL]);
            }
          }
        }
        return Types.UNDEFINED;
      } else {
        return new MultiType(newMulti as Type[])
      }
    } else if (leftExpressionType instanceof MultiType) {
      if (leftExpressionType.isCompatible(rightExpressionType as Type)) {
        return resultTypeAfterInfixOp(operator, rightExpressionType, rightExpressionType);
      } else {
        if (Config.enable_type_casting) {
          if (leftExpressionType.isCompatible(Types.INTEGER) || leftExpressionType.isCompatible(Types.REAL)) {
            if (rightExpressionType.isCompatible(Types.INTEGER) || rightExpressionType.isCompatible(Types.REAL)) {
              return rightExpressionType;
            }
          }
        }
        return Types.UNDEFINED;
      }
    } else if (rightExpressionType instanceof MultiType) {
      if (rightExpressionType.isCompatible(leftExpressionType as Type)) {
        return resultTypeAfterInfixOp(operator, leftExpressionType, leftExpressionType);
      } else {
        if (Config.enable_type_casting) {
          if (leftExpressionType.isCompatible(Types.INTEGER) || leftExpressionType.isCompatible(Types.REAL)) {
            if (rightExpressionType.isCompatible(Types.INTEGER) || rightExpressionType.isCompatible(Types.REAL)) {
              return leftExpressionType;
            }
          }
        }
        return Types.UNDEFINED;
      }
    }
    const resultTable = infixMap.get(operator) || [];
    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
    const resultType = resultTable[leftExpressionType.ord!][rightExpressionType.ord!];
    if (resultType === null || resultType === undefined) {
      if (Config.enable_type_casting) {
        if (leftExpressionType.isCompatible(Types.INTEGER) || leftExpressionType.isCompatible(Types.REAL)) {
          if (rightExpressionType.isCompatible(Types.INTEGER) || rightExpressionType.isCompatible(Types.REAL)) {
            if (operator === Operators.MOD) {
              return Types.INTEGER;
            } else if (operator.ord >= 5 && operator.ord <= 10) {
              return Types.BOOLEAN;
            }
          }
        }
      }
      return Types.UNDEFINED
    }
    return resultType;
  } catch (e) {
    if (e instanceof TypeError) {
      return Types.UNDEFINED;
    } else {
      throw e;
    }
  }
}

export function resultTypeAfterUnaryOp (operator: Operator, leftExpressionType: IType): IType {
  try {
    if (leftExpressionType instanceof MultiType) {
      let newMulti = [];
      for (let i = 0; i < leftExpressionType.types.length; ++i) {
        const type = leftExpressionType.types[i];
        newMulti.push(resultTypeAfterUnaryOp(operator, type));
      }
      newMulti = newMulti.filter(x => !x.isCompatible(Types.UNDEFINED));
      if (newMulti.length <= 0) {
        return Types.UNDEFINED;
      }
      return new MultiType(newMulti as Type[]);
    }
    const resultTable = unaryMap.get(operator) || [];
    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
    const resultType = resultTable[leftExpressionType.ord!];
    if (resultType == null) {
      return Types.UNDEFINED;
    }
    return resultType;
  } catch (e) {
    if (e instanceof TypeError) {
      return Types.UNDEFINED;
    } else {
      throw e;
    }
  }
}