import { DISTANCE_UNIT_TYPE, MAP_LOCALE_TYPE } from './CommandTypes';
import type {
  GEOJSON_PATH_TYPE,
  INSTRUCTION_TYPE,
  JIBESTREAM_PATH_TYPE,
} from './JibestreamTypes';
const SLOW_WALKING_SPEED = 1.18;

export function getCurrentInstruction(
  userLocation: number[],
  instructions: INSTRUCTION_TYPE[],
  dist_unit: DISTANCE_UNIT_TYPE = 'METERS',
  locale: MAP_LOCALE_TYPE = 'en-US'
) {
  // calculate pixel scaling factor
  var METERS_PER_PIXEL = 0.05962332392785686 * 4;
  if (
    instructions.length !== 0 &&
    instructions[0]!.distance !== undefined &&
    instructions[0]!.startPoint !== undefined &&
    instructions[0]!.endPoint !== undefined
  ) {
    var instruction = instructions[0]!;
    var dist = instruction.distance;
    var pixelDist = getDistanceBetweenPoints(
      instruction.startPoint!,
      instruction.endPoint!
    );
    METERS_PER_PIXEL = dist / pixelDist;
  }

  var closestInstruction:
    | {
        instruction: INSTRUCTION_TYPE;
        closestPointDist: number;
        withinPath: boolean;
        perpDist: number;
        distOffPath: number;
      }
    | undefined = undefined;
  instructions.forEach(function (instruction) {
    const closestPointDist = getDistanceFromPoints(
      userLocation,
      instruction.startPoint!,
      instruction.endPoint!
    );
    const perpDist = getPerpendicularDistanceFromPath(
      userLocation,
      instruction.startPoint!,
      instruction.endPoint!
    );
    const { distOffPath, ratioAlongPath } = getDistanceAlongPath(
      userLocation,
      instruction.startPoint!,
      instruction.endPoint!
    );
    const withinPath = ratioAlongPath > 0 && ratioAlongPath < 1;

    let remainingDistance = getDistanceBetweenPoints(
      userLocation,
      instruction.endPoint!
    );
    remainingDistance =
      Math.round(remainingDistance * METERS_PER_PIXEL * 100) / 100;
    const fullText = generateInstructionText(
      {
        ...instruction,
        remainingDistance,
      },
      dist_unit,
      locale
    );

    var isBetterInstruction = false;
    if (closestInstruction === undefined) {
      isBetterInstruction = true;
    } else {
      if (
        Math.abs(closestPointDist - closestInstruction.closestPointDist) < 0.1
      ) {
        // Start/end point of multiple paths are the same distance (near a corner)
        if (withinPath && !closestInstruction.withinPath) {
          // Outside corner of path, but only within current path bounds. Choose current path
          isBetterInstruction = true;
        } else if (!withinPath && closestInstruction.withinPath) {
          // outside corner of path, but only within previous path bounds. Keep previous path
          isBetterInstruction = false;
        } else if (withinPath && closestInstruction.withinPath) {
          // inside corner of path. Choose the instruction with the lowest perperndiclar distance
          if (perpDist < closestInstruction.perpDist) {
            isBetterInstruction = true;
          }
        } else {
          // Outside corner of path. Choose the instruction that the point is closest to being within (from distanceAlongPath distance)
          if (distOffPath < closestInstruction.distOffPath) {
            isBetterInstruction = true;
          }
        }
      } else if (closestPointDist < closestInstruction.closestPointDist) {
        // Closer to current instruction point
        isBetterInstruction = true;
      }
    }
    if (isBetterInstruction) {
      closestInstruction = {
        instruction: { ...instruction, remainingDistance, fullText },
        closestPointDist,
        withinPath,
        perpDist,
        distOffPath,
      };
    }
  });
  // @ts-ignore
  return closestInstruction?.instruction;
}

export function getSurroundingPoints(
  path: JIBESTREAM_PATH_TYPE,
  instructions: INSTRUCTION_TYPE[],
  waypointId: number
) {
  let instructionIndex = 0;
  let leadWaypointIndex = -1;

  for (let i = 0; i < path.points.length; i++) {
    if (path.points[i] && path.points[i]?.id == waypointId) {
      leadWaypointIndex = instructionIndex;
      break;
    }
    if (
      instructions[instructionIndex] &&
      path.points[i]?.id == instructions[instructionIndex]?.wp
    ) {
      instructionIndex += 1;
    }
  }
  return leadWaypointIndex;
}

export const preProcessPath = (
  path: JIBESTREAM_PATH_TYPE[],
  instructions: INSTRUCTION_TYPE[]
) => {
  var pathSegmentIndex = 0;
  var prevPathIndex = 0;
  var prevInstructionIndex: number;

  path.forEach(function (floorPath) {
    floorPath.points.forEach(function (pathSegment) {
      if (pathSegment.associations && pathSegment.associations?.amenities) {
        let foundDoor = false;

        // Check if this waypoint is a door
        pathSegment.associations?.amenities.forEach(function (amenity) {
          if (amenity == 13237) {
            foundDoor = true;
          }
        });

        // if the waypoint has a door find, inject a new nav instruction into the path.
        if (foundDoor) {
          const index = getSurroundingPoints(
            floorPath,
            instructions,
            pathSegment.id
          );
          const splitInstruction: INSTRUCTION_TYPE = {
            fullText: 'Proceed through the Door',
            prepend: '',
            direction: 'Proceed through the Door',
            distance: instructions[index]?.distance || 1,
            remainingDistance: instructions[index]?.remainingDistance || 1,
            wp: pathSegment.id,
            dest: -1,
            startPoint: [],
            endPoint: [],
          };
          instructions.splice(index, 0, splitInstruction);
        }
      }
    });
  });

  instructions.forEach(function (instruction, instructionIndex) {
    for (var i = pathSegmentIndex; i < path.length; i++) {
      const pathPoints = path[i]?.points;
      if (pathPoints === undefined) {
        continue;
      }
      for (var j = prevPathIndex; j < pathPoints.length; j++) {
        const point = pathPoints[j];
        if (point === undefined) {
          continue;
        }
        if (point.id === instruction.wp) {
          instruction.endPoint = [point.x, point.y];
          instruction.endPointLngLat = [point.longitude, point.latitude];
          if (prevInstructionIndex !== undefined) {
            instruction.startPoint =
              instructions[prevInstructionIndex]!.endPoint;
            instruction.startPointLngLat =
              instructions[prevInstructionIndex]!.endPointLngLat;
          } else {
            instruction.startPoint = [pathPoints[0]?.x!, pathPoints[0]?.y!];
            instruction.startPointLngLat = [
              pathPoints[0]?.longitude!,
              pathPoints[0]?.latitude!,
            ];
          }
          prevInstructionIndex = instructionIndex;
          return;
        }
        prevPathIndex = j;
      }
      pathSegmentIndex = i;
      prevPathIndex = 0;
    }
  });
  return instructions;
};

export const generateGeojsonPath = (
  processedInstructions: INSTRUCTION_TYPE[],
  jibestreamPath: JIBESTREAM_PATH_TYPE[],
  distUnit: DISTANCE_UNIT_TYPE,
  locale: MAP_LOCALE_TYPE
): GEOJSON_PATH_TYPE => {
  const coordinates: number[][] = [];
  jibestreamPath.forEach((path, floorIndex) => {
    path.points.forEach((point, pointIndex) => {
      if (floorIndex == 0 && pointIndex == 0) {
        coordinates.push([point.longitude, point.latitude]);
      } else if (
        processedInstructions.some((value: INSTRUCTION_TYPE) => {
          return value.wp == point.id;
        })
      )
        coordinates.push([point.longitude, point.latitude]);
    });
  });

  let totalDistance = 0;
  const steps: GEOJSON_PATH_TYPE['steps'] = [];
  for (let i = 0; i < processedInstructions.length; i++) {
    const instruction = processedInstructions[i]!;
    totalDistance += instruction.distance;
    const endPointLngLat = instruction.endPointLngLat!;
    const step: GEOJSON_PATH_TYPE['steps'][0] = {
      distance: instruction.distance,
      directions: generateInstructionText(instruction, distUnit, locale),
      lon: endPointLngLat[0]!,
      lat: endPointLngLat[1]!,
      prepend: instruction.prepend,
      direction: instruction.direction,
    };
    steps.push(step);
  }
  const duration = Math.round((totalDistance / SLOW_WALKING_SPEED) * 100) / 100;
  const startTime = Date.now();
  const endTime = startTime + duration * 1000;

  const path: GEOJSON_PATH_TYPE = {
    distance: totalDistance,
    duration,
    startTime,
    endTime,
    legGeometry: {
      properties: {
        name: 'wayfind-path-geojson',
      },
      type: 'Feature',
      geometry: {
        type: 'LineString',
        coordinates,
      },
    },
    steps: steps,
  };

  return path;
};

// get distance along path as proportion of total path length
export function getDistanceAlongPath(
  point: number[],
  startPathPoint: number[],
  endPathPoint: number[]
) {
  const x1 = startPathPoint[0]!;
  const y1 = startPathPoint[1]!;
  const x2 = endPathPoint[0]!;
  const y2 = endPathPoint[1]!;
  const x0 = point[0]!;
  const y0 = point[1]!;
  const numerator = (x2 - x1) * (x0 - x1) + (y2 - y1) * (y0 - y1); // dot product
  const denominator = Math.pow(y2 - y1, 2) + Math.pow(x2 - x1, 2);
  const totalPathDist = Math.sqrt(denominator);
  const ratioAlongPath = numerator / denominator;
  const distanceAlongPath = ratioAlongPath * totalPathDist;
  var distOffPath = 0;
  if (ratioAlongPath < 0) {
    distOffPath = -distanceAlongPath;
  } else if (ratioAlongPath > 1) {
    distOffPath = distanceAlongPath - totalPathDist;
  }

  return {
    distanceAlongPath,
    ratioAlongPath,
    totalPathDist,
    distOffPath,
  };
}

// Get perpendicular distance from point to path
export function getPerpendicularDistanceFromPath(
  point: number[],
  startPathPoint: number[],
  endPathPoint: number[]
) {
  const x1 = startPathPoint[0]!;
  const y1 = startPathPoint[1]!;
  const x2 = endPathPoint[0]!;
  const y2 = endPathPoint[1]!;
  const x0 = point[0]!;
  const y0 = point[1]!;
  const numerator = Math.abs(
    (y2 - y1) * x0 - (x2 - x1) * y0 + x2 * y1 - y2 * x1
  );
  const denominator = Math.sqrt(Math.pow(y2 - y1, 2) + Math.pow(x2 - x1, 2));
  return numerator / denominator;
}

// Get perpendicular distance from point to path
export function getDistanceFromPoints(
  point: number[],
  startPathPoint: number[],
  endPathPoint: number[]
) {
  return Math.min(
    getDistanceBetweenPoints(point, startPathPoint),
    getDistanceBetweenPoints(point, endPathPoint)
  );
}

export function getDistanceBetweenPoints(point1: number[], point2: number[]) {
  const x1 = point1[0]!;
  const y1 = point1[1]!;
  const x2 = point2[0]!;
  const y2 = point2[1]!;
  return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
}

export function generateInstructionText(
  instruction: INSTRUCTION_TYPE,
  dist_unit: DISTANCE_UNIT_TYPE,
  locale: MAP_LOCALE_TYPE
) {
  const distance = Math.ceil(
    convertDistanceUnit(instruction.remainingDistance, dist_unit)
  );
  const prepend = convertPrepend(instruction.prepend, locale);
  const direction = convertDirection(instruction.direction, locale);
  const atTranslated = locale === 'es-ES' ? 'en' : 'at';
  const destinationName = instruction.destinationName;
  const inTranslated = locale === 'es-ES' ? 'en' : 'in';
  const unitName = convertUnitName(dist_unit, locale, distance != 1);
  if (destinationName)
    return `${prepend}${direction} ${atTranslated} ${destinationName} ${inTranslated} ${distance} ${unitName}`;
  else return `${prepend}${direction} ${inTranslated} ${distance} ${unitName}`;
}

export function convertDistanceUnit(
  distance: number,
  dist_unit: DISTANCE_UNIT_TYPE
): number {
  switch (dist_unit) {
    case 'METERS':
      return distance;
    case 'FEET':
      return distance * 3.28084;
    case 'INCHES':
      return distance * 39.3701;
    default:
      return distance;
  }
}

export function convertPrepend(
  prepend: string,
  locale: MAP_LOCALE_TYPE
): string {
  switch (prepend) {
    case 'Turn ':
      switch (locale) {
        case 'es-ES':
          return 'Gire ';
        default:
          return prepend;
      }
    case 'Arrive ':
      switch (locale) {
        case 'es-ES':
          return 'Llegue ';
        default:
          return prepend;
      }
    default:
      return prepend;
  }
}

export function convertDirection(
  prepend: string,
  locale: MAP_LOCALE_TYPE
): string {
  switch (prepend) {
    case 'Left':
      switch (locale) {
        case 'es-ES':
          return 'Izquierdo';
        default:
          return prepend;
      }
    case 'Right':
      switch (locale) {
        case 'es-ES':
          return 'Derecho';
        default:
          return prepend;
      }
    case 'Forward':
      switch (locale) {
        case 'es-ES':
          return 'hacia adelante';
        default:
          return prepend;
      }
    default:
      return prepend;
  }
}

export function convertUnitName(
  unit: DISTANCE_UNIT_TYPE,
  locale: MAP_LOCALE_TYPE,
  plural: boolean
): string {
  switch (unit) {
    case 'METERS':
      switch (locale) {
        case 'es-ES':
          return plural ? 'metros' : 'metro';
        default:
          return plural ? 'meters' : 'meter';
      }
    case 'FEET':
      switch (locale) {
        case 'es-ES':
          return plural ? 'pies' : 'pie';
        default:
          return plural ? 'feet' : 'foot';
      }
    case 'INCHES':
      switch (locale) {
        case 'es-ES':
          return plural ? 'pulgada' : 'pulgadas';
        default:
          return plural ? 'inches' : 'inch';
      }
    default:
      return unit!;
  }
}
