/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { Modes } from '../modes';
import { Types } from "../../typeSystem/types";
import { StoreObject } from './storeObject';
import { IType } from '../../typeSystem/itype';
import { StoreObjectRef } from './storeObjectRef';
import { ArrayType } from '../../typeSystem/array_type';
import { ArrayStoreValue } from './value/array_store_value';
import { Location } from '../../memory/location';
import { StoreObjectArray } from './storeObjectArray';
import { IStoreValue } from './value/istore_value';
import { StoreValue } from './value/store_value';
import { StoreValueAddress } from './value/store_value_address';
import { StoreValueRef } from './value/store_value_ref';
import { ArrayStoreValueRef } from './value/array_store_value_ref';
import { StoreObjectArrayRef } from './store_object_array_ref';

export class Store {

  static canImplicitTypeCast (castType: IType, sourceType: IType): boolean {
    if (castType.isCompatible(Types.INTEGER) || castType.isCompatible(Types.REAL)) {
      if (sourceType.isCompatible(Types.INTEGER) || sourceType.isCompatible(Types.REAL)) {
        return true;
      }
    }
    return false;
  }

  static doImplicitCasting (castType: IType, stoObj: IStoreValue): IStoreValue {
    if(!Store.canImplicitTypeCast(castType, stoObj.type)) {
      throw new Error("!!!Critical error: attempted to type cast invalid types");
    }
    if(castType.isCompatible(Types.INTEGER)) {
      return new StoreValue(Types.INTEGER, stoObj.get().trunc());
    } else {
      return new StoreValue(Types.REAL, stoObj.get());
    }
  }

  private store: Map<string, StoreObject>;
  public nextStore?: Store
  public mode: symbol;

  constructor(public name: string) {
    this.store = new Map<string, StoreObject>();
    this.mode = Modes.RUN; 
  }

  extendStore (nextStore: Store): void {
    this.nextStore = nextStore;
  }

  applyStore (id: string): IStoreValue {
    if (!this.store.has(id)) {
      if (this.nextStore != null) {
        return this.nextStore.applyStore(id);
      } else {
        throw new Error(`Variable ${id} not found.`);
      }
    }
    const val = this.store.get(id)!;
    let result = null
    if (val.type instanceof ArrayType) {
      const array = val as StoreObjectArray;
      const array_type = array.type as ArrayType;
      let l = 0, c = 0;
      const values = array.value.map( v => {
        if(array.isVector) {
          return new StoreValueAddress(array_type.innerType, v, l++, undefined, array.id, array.readOnly);  
        } else {
          if(c >= array.columns) {
            c = 0;
            l += 1;
          }
          return new StoreValueAddress(array_type.innerType, v, l, c++, array.id, array.readOnly);  
        }
      });
      result = new ArrayStoreValue(array_type, values, array.lines, array.columns, val.id, val.readOnly);
    } else {
      result = new StoreValue(val.type, val.value, val.id, val.readOnly);
    }
    
    return result;
  }

  updateStore (id: string, stoValue: IStoreValue): Store {
    if (!this.store.has(id)) {
      if (this.nextStore != null) {
        this.nextStore.updateStore(id, stoValue);
        return this;
      } else {
        // TODO: better error message
        throw new Error(`Variable ${id} not found.`);
      }
    } else {
      const oldObj = this.store.get(id)!;
      if (oldObj.readOnly) {
        // TODO: better error message
        throw new Error("Cannot change value of a read only variable: " + id);
      }

      if (oldObj instanceof StoreObjectArray) {
        const array_value = stoValue as ArrayStoreValue;
        if(oldObj.isCompatible(array_value)) {
          if(oldObj.isVector) {
            array_value.get().forEach((val, index) => {
              oldObj.setAt(val, index, undefined);
            });
          } else {
            let line = 0;
            let column = 0;
            array_value.get().forEach((val) => {
              oldObj.setAt(val, line, column);
              column += 1;
              if(column >= oldObj.columns) {
                line += 1;
                column = 0;
              }
            });
          }
          return this;
        }
      } else if (oldObj.isCompatible(stoValue)) {
        const loc_address = oldObj.locAddress;
        Location.updateAddress(loc_address, stoValue.get());
        return this;
      }
      const oldType = oldObj.type;
      const stoType = stoValue.type;
      // TODO: better error message
      throw new Error(`${oldType.value} is not compatible with type ${stoType.value} given`);
      
    }
  }

  /**
   * Method used to update regions of an array (vector or matrix). The should only be used when update an specific
   * possition since it will only update the required addresses.
   * @param {string} id the variable id to be updated
   * @param {IStoreValue} sto_value the value to be used in the update process
   * @param {number} line the line address of the vector/matrix
   * @param {number} column the matrix column, which can be undefined
   */
  updateStoreArray (id: string, sto_value: IStoreValue, line: number, column?: number): Store {
    if (!this.store.has(id)) {
      if (this.nextStore != null) {
        this.nextStore.updateStoreArray(id, sto_value, line, column);
        return this;
      } else {
        // TODO: better error message
        throw new Error(`Variable ${id} not found.`);
      }
    } else {
      const oldObj = this.store.get(id)!;
      if (oldObj.readOnly) {
        // TODO: better error message
        throw new Error("Cannot change value of a read only variable: " + id);
      }

      if (oldObj instanceof StoreObjectArray) {
        if(sto_value instanceof ArrayStoreValue) {
          // this must be a vector  or matrix line update
          const actual_values =  sto_value.get();
          if(oldObj.isVector && sto_value.isVector()) {
            for(let i = 0;i < sto_value.lines; i += 1) {
              const val = actual_values[i]
              oldObj.setAt(val, i, undefined);
            }
          } else if(!oldObj.isVector && column == null && sto_value.isVector()) {
            for(let i = 0;i < oldObj.columns; i += 1) {
              const val = actual_values[i]
              oldObj.setAt(val, line, i);
            }
          } else {
            // TODO: better error message
            throw new Error(`Attempting to assign an invalid value to array ${id}`);
          }
        } else {
          if(!oldObj.isVector && column == null) {
            // TODO: better error message
            throw new Error(`Attempting to assign an invalid value to array ${id}`);
          }
          oldObj.setAt(sto_value as StoreValue, line, column);
        }
      } else {
        throw new Error("Cannot update a non-array variable using updateStoreArray");
      }
      // const oldType = oldObj.type;
      // const stoType = sto_value.type;
      // // TODO: better error message
      // throw new Error(`${oldType.value} is not compatible with type ${stoType.value} given`);
      return this;
    }
  }

  /**
   * Inserts a new variable into the Store. This method should be used when declaring a new variable,
   * including the special return variable $.
   * @param id variable id
   * @param stoValue the value to be used as the initial value of id
   */
  insertStore (id: string, stoValue: IStoreValue): Store {
    if (this.store.has(id)) {
      // TODO: better error message
      throw new Error(`${id} is already defined`);
    }
    // TODO check for array....
    let newObj: StoreObject;
    if(stoValue instanceof StoreValueRef) {
      newObj = new StoreObjectRef(stoValue);
    } else if (stoValue instanceof ArrayStoreValueRef) {
      newObj = new StoreObjectArrayRef(stoValue, stoValue.lines, stoValue.columns);
    } else if (stoValue instanceof ArrayStoreValue) {
      const columns = stoValue.isVector() ? 0 : stoValue.columns!;
      const addresses: number[] = [];
      const all_values = stoValue.get();
      if(all_values.length > 0) {
        for(let i = 0; i < stoValue.get().length; i += 1) {
          const val = all_values[i].get();
          addresses.push(Location.allocate(val));
        }
      } else {
        let total = stoValue.lines;
        total = stoValue.isVector() ? total : total * columns;
        for(let i = 0; i < total; i += 1) {
          addresses.push(Location.allocate(null));
        }
      }
      newObj = new StoreObjectArray(stoValue.type as ArrayType, stoValue.lines, columns, addresses, stoValue.isConst);
    } else {
      const loc_address = Location.allocate(stoValue.get());
      newObj = new StoreObject(stoValue.type, loc_address, stoValue.isConst);
    }
    newObj.setID(id);
    this.store.set(id, newObj);
    return this;
  }
  /**
   * Helper function similar to applyStore. But it returns the actual object in the store be it ref or not
   * applyStore will return the refferenced object if the object in the store is a ref
   */
  getStoreObject (id: string): StoreObject {
    if (!this.store.has(id)) {
      if (this.nextStore != null) {
        return this.nextStore.getStoreObject(id);
      } else {
        throw new Error(`Variable ${id} not found.`);
      }
    }
    return this.store.get(id)!;
  }

  destroy (): void {
    this.store.forEach(sto => sto.destroy(), this);
  }

  isDefined (id: string): boolean {
    if (!this.store.has(id)) {
      if (this.nextStore != null) {
        return this.nextStore.isDefined(id);
      } else {
        return false;
      }
    }
    return true;
  }
}