package org.checkerframework.checker.units;

import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.util.Elements;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.checker.units.qual.N;
import org.checkerframework.checker.units.qual.Prefix;
import org.checkerframework.checker.units.qual.g;
import org.checkerframework.checker.units.qual.h;
import org.checkerframework.checker.units.qual.kg;
import org.checkerframework.checker.units.qual.km2;
import org.checkerframework.checker.units.qual.km3;
import org.checkerframework.checker.units.qual.kmPERh;
import org.checkerframework.checker.units.qual.m;
import org.checkerframework.checker.units.qual.m2;
import org.checkerframework.checker.units.qual.m3;
import org.checkerframework.checker.units.qual.mPERs;
import org.checkerframework.checker.units.qual.mPERs2;
import org.checkerframework.checker.units.qual.mm2;
import org.checkerframework.checker.units.qual.mm3;
import org.checkerframework.checker.units.qual.s;
import org.checkerframework.checker.units.qual.t;
import org.checkerframework.framework.type.AnnotatedTypeMirror;

/** Default relations between SI units. */
public class UnitsRelationsDefault implements UnitsRelations {
  /** SI base units. */
  @SuppressWarnings("nullness:initialization.field.uninitialized") // init() method
  protected AnnotationMirror m, km, mm, s, g, kg;

  /** Derived SI units without special names */
  @SuppressWarnings("nullness:initialization.field.uninitialized") // init() method
  protected AnnotationMirror m2, km2, mm2, m3, km3, mm3, mPERs, mPERs2;

  /** Derived SI units with special names */
  @SuppressWarnings("nullness:initialization.field.uninitialized") // init() method
  protected AnnotationMirror N, kN;

  /** Non-SI units */
  @SuppressWarnings("nullness:initialization.field.uninitialized") // init() method
  protected AnnotationMirror h, kmPERh, t;

  /** The Element Utilities from the Units Checker's processing environment. */
  @SuppressWarnings("nullness:initialization.field.uninitialized") // init() method
  protected Elements elements;

  /**
   * Constructs various AnnotationMirrors representing specific checker-framework provided Units
   * involved in the rules resolved in this UnitsRelations implementation.
   */
  @Override
  public UnitsRelations init(ProcessingEnvironment env) {
    elements = env.getElementUtils();

    m = UnitsRelationsTools.buildAnnoMirrorWithDefaultPrefix(env, m.class);
    km = UnitsRelationsTools.buildAnnoMirrorWithSpecificPrefix(env, m.class, Prefix.kilo);
    mm = UnitsRelationsTools.buildAnnoMirrorWithSpecificPrefix(env, m.class, Prefix.milli);

    m2 = UnitsRelationsTools.buildAnnoMirrorWithNoPrefix(env, m2.class);
    km2 = UnitsRelationsTools.buildAnnoMirrorWithNoPrefix(env, km2.class);
    mm2 = UnitsRelationsTools.buildAnnoMirrorWithNoPrefix(env, mm2.class);

    m3 = UnitsRelationsTools.buildAnnoMirrorWithNoPrefix(env, m3.class);
    km3 = UnitsRelationsTools.buildAnnoMirrorWithNoPrefix(env, km3.class);
    mm3 = UnitsRelationsTools.buildAnnoMirrorWithNoPrefix(env, mm3.class);

    s = UnitsRelationsTools.buildAnnoMirrorWithDefaultPrefix(env, s.class);
    h = UnitsRelationsTools.buildAnnoMirrorWithNoPrefix(env, h.class);

    mPERs = UnitsRelationsTools.buildAnnoMirrorWithNoPrefix(env, mPERs.class);
    kmPERh = UnitsRelationsTools.buildAnnoMirrorWithNoPrefix(env, kmPERh.class);

    mPERs2 = UnitsRelationsTools.buildAnnoMirrorWithNoPrefix(env, mPERs2.class);

    g = UnitsRelationsTools.buildAnnoMirrorWithDefaultPrefix(env, g.class);
    kg = UnitsRelationsTools.buildAnnoMirrorWithSpecificPrefix(env, g.class, Prefix.kilo);
    t = UnitsRelationsTools.buildAnnoMirrorWithNoPrefix(env, t.class);
    N = UnitsRelationsTools.buildAnnoMirrorWithDefaultPrefix(env, N.class);
    kN = UnitsRelationsTools.buildAnnoMirrorWithSpecificPrefix(env, N.class, Prefix.kilo);

    return this;
  }

  /**
   * Provides rules for resolving the result Unit of the multiplication of checker-framework
   * provided Units.
   */
  @Override
  public @Nullable AnnotationMirror multiplication(
      AnnotatedTypeMirror lht, AnnotatedTypeMirror rht) {
    // TODO: does this handle scaling correctly?

    // length * length => area
    // checking SI units only
    if (UnitsRelationsTools.hasSpecificUnitIgnoringPrefix(lht, m)
        && UnitsRelationsTools.hasSpecificUnitIgnoringPrefix(rht, m)) {
      if (UnitsRelationsTools.hasNoPrefix(lht) && UnitsRelationsTools.hasNoPrefix(rht)) {
        // m * m
        return m2;
      }

      Prefix lhtPrefix = UnitsRelationsTools.getPrefix(lht);
      Prefix rhtPrefix = UnitsRelationsTools.getPrefix(rht);

      if (bothHaveSpecificPrefix(lhtPrefix, rhtPrefix, Prefix.kilo)) {
        // km * km
        return km2;
      } else if (bothHaveSpecificPrefix(lhtPrefix, rhtPrefix, Prefix.one)) {
        // m(Prefix.one) * m(Prefix.one)
        return m2;
      } else if (bothHaveSpecificPrefix(lhtPrefix, rhtPrefix, Prefix.milli)) {
        // mm * mm
        return mm2;
      } else {
        return null;
      }
    } else if (havePairOfUnitsIgnoringOrder(lht, m, rht, m2)) {
      return m3;
    } else if (havePairOfUnitsIgnoringOrder(lht, km, rht, km2)) {
      return km3;
    } else if (havePairOfUnitsIgnoringOrder(lht, mm, rht, mm2)) {
      return mm3;
    } else if (havePairOfUnitsIgnoringOrder(lht, s, rht, mPERs)) {
      // s * mPERs or mPERs * s => m
      return m;
    } else if (havePairOfUnitsIgnoringOrder(lht, s, rht, mPERs2)) {
      // s * mPERs2 or mPERs2 * s => mPERs
      return mPERs;
    } else if (havePairOfUnitsIgnoringOrder(lht, h, rht, kmPERh)) {
      // h * kmPERh or kmPERh * h => km
      return km;
    } else if (havePairOfUnitsIgnoringOrder(lht, kg, rht, mPERs2)) {
      // kg * mPERs2 or mPERs2 * kg = N
      return N;
    } else if (havePairOfUnitsIgnoringOrder(lht, t, rht, mPERs2)) {
      // t * mPERs2 or mPERs2 * t = kN
      return kN;
    } else {
      return null;
    }
  }

  /**
   * Provides rules for resolving the result Unit of the division of checker-framework provided
   * Units.
   */
  @Override
  public @Nullable AnnotationMirror division(AnnotatedTypeMirror lht, AnnotatedTypeMirror rht) {
    if (havePairOfUnits(lht, m, rht, s)) {
      // m / s => mPERs
      return mPERs;
    } else if (havePairOfUnits(lht, km, rht, h)) {
      // km / h => kmPERh
      return kmPERh;
    } else if (havePairOfUnits(lht, m2, rht, m)) {
      // m2 / m => m
      return m;
    } else if (havePairOfUnits(lht, km2, rht, km)) {
      // km2 / km => km
      return km;
    } else if (havePairOfUnits(lht, mm2, rht, mm)) {
      // mm2 / mm => mm
      return mm;
    } else if (havePairOfUnits(lht, m3, rht, m)) {
      // m3 / m => m2
      return m2;
    } else if (havePairOfUnits(lht, km3, rht, km)) {
      // km3 / km => km2
      return km2;
    } else if (havePairOfUnits(lht, mm3, rht, mm)) {
      // mm3 / mm => mm2
      return mm2;
    } else if (havePairOfUnits(lht, m3, rht, m2)) {
      // m3 / m2 => m
      return m;
    } else if (havePairOfUnits(lht, km3, rht, km2)) {
      // km3 / km2 => km
      return km;
    } else if (havePairOfUnits(lht, mm3, rht, mm2)) {
      // mm3 / mm2 => mm
      return mm;
    } else if (havePairOfUnits(lht, m, rht, mPERs)) {
      // m / mPERs => s
      return s;
    } else if (havePairOfUnits(lht, km, rht, kmPERh)) {
      // km / kmPERh => h
      return h;
    } else if (havePairOfUnits(lht, mPERs, rht, s)) {
      // mPERs / s = mPERs2
      return mPERs2;
    } else if (havePairOfUnits(lht, mPERs, rht, mPERs2)) {
      // mPERs / mPERs2 => s  (velocity / acceleration == time)
      return s;
    } else if (UnitsRelationsTools.hasSpecificUnit(lht, N)) {
      if (UnitsRelationsTools.hasSpecificUnit(rht, kg)) {
        // N / kg => mPERs2
        return mPERs2;
      } else if (UnitsRelationsTools.hasSpecificUnit(rht, mPERs2)) {
        // N / mPERs2 => kg
        return kg;
      }
      return null;
    } else if (UnitsRelationsTools.hasSpecificUnit(lht, kN)) {
      if (UnitsRelationsTools.hasSpecificUnit(rht, t)) {
        // kN / t => mPERs2
        return mPERs2;
      } else if (UnitsRelationsTools.hasSpecificUnit(rht, mPERs2)) {
        // kN / mPERs2 => t
        return t;
      }
      return null;
    } else {
      return null;
    }
  }

  /**
   * Checks to see if both lhtPrefix and rhtPrefix have the same prefix as specificPrefix.
   *
   * @param lhtPrefix left hand side prefix
   * @param rhtPrefix right hand side prefix
   * @param specificPrefix specific desired prefix to match
   * @return true if all 3 Prefix are the same, false otherwise
   */
  protected boolean bothHaveSpecificPrefix(
      Prefix lhtPrefix, Prefix rhtPrefix, Prefix specificPrefix) {
    if (lhtPrefix == null || rhtPrefix == null || specificPrefix == null) {
      return false;
    }

    return lhtPrefix == rhtPrefix && rhtPrefix == specificPrefix;
  }

  /**
   * Checks to see if lht has the unit ul and if rht has the unit ur all at the same time.
   *
   * @param lht left hand annotated type
   * @param ul left hand unit
   * @param rht right hand annotated type
   * @param ur right hand unit
   * @return true if lht has lu and rht has ru, false otherwise
   */
  protected boolean havePairOfUnits(
      AnnotatedTypeMirror lht, AnnotationMirror ul, AnnotatedTypeMirror rht, AnnotationMirror ur) {
    return UnitsRelationsTools.hasSpecificUnit(lht, ul)
        && UnitsRelationsTools.hasSpecificUnit(rht, ur);
  }

  /**
   * Checks to see if lht and rht have the pair of units u1 and u2 regardless of order.
   *
   * @param lht left hand annotated type
   * @param u1 unit 1
   * @param rht right hand annotated type
   * @param u2 unit 2
   * @return true if lht and rht have the pair of units u1 and u2 regardless of order, false
   *     otherwise
   */
  protected boolean havePairOfUnitsIgnoringOrder(
      AnnotatedTypeMirror lht, AnnotationMirror u1, AnnotatedTypeMirror rht, AnnotationMirror u2) {
    return havePairOfUnits(lht, u1, rht, u2) || havePairOfUnits(lht, u2, rht, u1);
  }
}
