// Route response validation and geometry processing utilities
import { ensureDecimal } from '../lib/utils.js';
import { env } from './env.js';
import { sendRequest } from './http.js';

export default class Route {
  constructor(world) {
    this.world = world;
  }

  paramsToQuery(params) {
    let query = '';
    if (params.coordinates !== undefined) {
      // FIXME this disables passing the output if its a default
      // Remove after #2173 is fixed.
      const outputString =
        params.output && params.output !== 'json' ? `.${params.output}` : '';
      query = params.coordinates.join(';') + outputString;
      delete params.coordinates;
      delete params.output;
    }
    if (Object.keys(params).length) {
      query += `?${Object.keys(params)
        .map((k) => `${k}=${params[k]}`)
        .join('&')}`;
    }

    return query;
  }

  requestPath(service, parameters, callback) {
    const baseUrl = new URL(`${service}/v1/${this.profile}/`, env.wp.host);
    const query = this.paramsToQuery(parameters);
    const url = new URL(query, baseUrl);
    sendRequest(url, this.log, callback);
  }

  requestUrl(path, callback) {
    const url = new URL(path, env.wp.host);
    sendRequest(url, this.log, callback);
  }

  // Overwrites the default values in defaults
  // e.g. [[a, 1], [b, 2]], [[a, 5], [d, 10]] => [[a, 5], [b, 2], [d, 10]]
  overwriteParams(defaults, other) {
    const otherMap = {};
    for (const key in other) otherMap[key] = other[key];
    return Object.assign({}, defaults, otherMap);
  }

  encodeWaypoints(waypoints) {
    return waypoints.map((w) => [w.lon, w.lat].map(ensureDecimal).join(','));
  }

  requestRoute(waypoints, bearings, approaches, userParams, callback) {
    if (bearings.length && bearings.length !== waypoints.length)
      throw new Error(
        '*** number of bearings does not equal the number of waypoints',
      );
    if (approaches.length && approaches.length !== waypoints.length)
      throw new Error(
        '*** number of approaches does not equal the number of waypoints',
      );

    const defaults = {
        output: 'json',
        steps: 'true',
        alternatives: 'false',
      },
      params = this.overwriteParams(defaults, userParams),
      encodedWaypoints = this.encodeWaypoints(waypoints);

    params.coordinates = encodedWaypoints;

    if (bearings.length) {
      params.bearings = bearings
        .map((b) => {
          const bs = b.split(',');
          if (bs.length === 2) return b;
          else return (b += ',10');
        })
        .join(';');
    }

    if (approaches.length) {
      params.approaches = approaches.join(';');
    }
    return this.requestPath('route', params, callback);
  }

  requestNearest(node, userParams, callback) {
    const defaults = {
        output: 'json',
      },
      params = this.overwriteParams(defaults, userParams);
    params.coordinates = [[node.lon, node.lat].join(',')];

    return this.requestPath('nearest', params, callback);
  }

  requestTable(waypoints, userParams, callback) {
    const defaults = {
        output: 'json',
      },
      params = this.overwriteParams(defaults, userParams);

    params.coordinates = waypoints.map((w) =>
      [w.coord.lon, w.coord.lat].join(','),
    );
    const srcs = waypoints
        .map((w, i) => [w.type, i])
        .filter((w) => w[0] === 'src')
        .map((w) => w[1]),
      dsts = waypoints
        .map((w, i) => [w.type, i])
        .filter((w) => w[0] === 'dst')
        .map((w) => w[1]);
    if (srcs.length) params.sources = srcs.join(';');
    if (dsts.length) params.destinations = dsts.join(';');

    return this.requestPath('table', params, callback);
  }

  requestTrip(waypoints, userParams, callback) {
    const defaults = {
        output: 'json',
        steps: 'true',
      },
      params = this.overwriteParams(defaults, userParams);

    params.coordinates = this.encodeWaypoints(waypoints);

    return this.requestPath('trip', params, callback);
  }

  requestMatching(waypoints, timestamps, userParams, callback) {
    const defaults = {
        output: 'json',
      },
      params = this.overwriteParams(defaults, userParams);

    params.coordinates = this.encodeWaypoints(waypoints);

    if (timestamps.length) {
      params.timestamps = timestamps.join(';');
    }

    return this.requestPath('match', params, callback);
  }

  extractInstructionList(instructions, keyFinder) {
    if (instructions) {
      return instructions.legs
        .reduce((m, v) => m.concat(v.steps), [])
        .map(keyFinder)
        .join(',');
    }
  }

  summary(instructions) {
    if (instructions) {
      return instructions.legs.map((l) => l.summary).join(';');
    }
  }

  wayList(instructions) {
    return this.extractInstructionList(instructions, (s) => s.name);
  }

  refList(instructions) {
    return this.extractInstructionList(instructions, (s) => s.ref || '');
  }

  pronunciationList(instructions) {
    return this.extractInstructionList(
      instructions,
      (s) => s.pronunciation || '',
    );
  }

  destinationsList(instructions) {
    return this.extractInstructionList(
      instructions,
      (s) => s.destinations || '',
    );
  }

  exitsList(instructions) {
    return this.extractInstructionList(instructions, (s) => s.exits || '');
  }

  reverseBearing(bearing) {
    if (bearing >= 180) return bearing - 180;
    return bearing + 180;
  }

  bearingList(instructions) {
    return this.extractInstructionList(
      instructions,
      (s) =>
        `${
          'in' in s.intersections[0]
            ? this.reverseBearing(
              s.intersections[0].bearings[s.intersections[0].in],
            )
            : 0
        }->${
          'out' in s.intersections[0]
            ? s.intersections[0].bearings[s.intersections[0].out]
            : 0
        }`,
    );
  }

  lanesList(instructions) {
    return this.extractInstructionList(instructions, (s) => {
      return s.intersections
        .map((i) => {
          if (i.lanes) {
            return i.lanes
              .map((l) => {
                const indications = l.indications.join(';');
                return `${indications}:${l.valid ? 'true' : 'false'}`;
              })
              .join(' ');
          } else {
            return '';
          }
        })
        .join(';');
    });
  }

  approachList(instructions) {
    return this.extractInstructionList(instructions, (s) => s.approaches || '');
  }

  annotationList(instructions) {
    if (!('annotation' in instructions.legs[0])) return '';

    const merged = {};
    instructions.legs.map((l) => {
      Object.keys(l.annotation)
        .filter((a) => !a.match(/metadata/))
        .forEach((a) => {
          if (!merged[a]) merged[a] = [];
          merged[a].push(l.annotation[a].join(':'));
        });
      if (l.annotation.metadata) {
        merged.metadata = {};
        Object.keys(l.annotation.metadata).forEach((a) => {
          if (!merged.metadata[a]) merged.metadata[a] = [];
          merged.metadata[a].push(l.annotation.metadata[a].join(':'));
        });
      }
    });
    Object.keys(merged)
      .filter((k) => !k.match(/metadata/))
      .map((a) => {
        merged[a] = merged[a].join(',');
      });
    if (merged.metadata) {
      Object.keys(merged.metadata).map((a) => {
        merged.metadata[a] = merged.metadata[a].join(',');
      });
    }
    return merged;
  }

  alternativesList(instructions) {
    // alternatives_count come from tracepoints list
    return instructions.tracepoints
      .map((t) => t.alternatives_count.toString())
      .join(',');
  }

  turnList(instructions) {
    return instructions.legs
      .reduce((m, v) => m.concat(v.steps), [])
      .map((v) => {
        switch (v.maneuver.type) {
        case 'depart':
        case 'arrive':
          return v.maneuver.type;
        case 'on ramp':
        case 'off ramp':
          return `${v.maneuver.type} ${v.maneuver.modifier}`;
        case 'roundabout':
          return `roundabout-exit-${v.maneuver.exit}`;
        case 'rotary':
          if ('rotary_name' in v)
            return `${v.rotary_name}-exit-${v.maneuver.exit}`;
          else return `rotary-exit-${v.maneuver.exit}`;
        case 'roundabout turn':
          return `${v.maneuver.type} ${v.maneuver.modifier} exit-${v.maneuver.exit}`;
          // FIXME this is a little bit over-simplistic for merge/fork instructions
        default:
          return `${v.maneuver.type} ${v.maneuver.modifier}`;
        }
      })
      .join(',');
  }

  locations(instructions) {
    return instructions.legs
      .reduce((m, v) => m.concat(v.steps), [])
      .map((v) => {
        return this.findNodeByLocation(v.maneuver.location);
      })
      .join(',');
  }

  intersectionList(instructions) {
    return instructions.legs
      .reduce((m, v) => m.concat(v.steps), [])
      .map((v) => {
        return v.intersections
          .map((intersection) => {
            let string = `${intersection.entry[0]}:${intersection.bearings[0]}`,
              i;
            for (i = 1; i < intersection.bearings.length; ++i)
              string = `${string} ${intersection.entry[i]}:${intersection.bearings[i]}`;
            return string;
          })
          .join(',');
      })
      .join(';');
  }

  modeList(instructions) {
    return this.extractInstructionList(instructions, (s) => s.mode);
  }

  drivingSideList(instructions) {
    return this.extractInstructionList(instructions, (s) => s.driving_side);
  }

  classesList(instructions) {
    return this.extractInstructionList(
      instructions,
      (s) =>
        `[${s.intersections.map((i) => `(${i.classes ? i.classes.join(',') : ''})`).join(',')}]`,
    );
  }

  timeList(instructions) {
    return this.extractInstructionList(instructions, (s) => `${s.duration}s`);
  }

  distanceList(instructions) {
    return this.extractInstructionList(instructions, (s) => `${s.distance}m`);
  }

  weightName(instructions) {
    return instructions ? instructions.weight_name : '';
  }

  weightList(instructions) {
    return this.extractInstructionList(instructions, (s) => s.weight);
  }
}
