/**
 * h is calculate by Euclidean Distance.
 * Complexity can be imporved by using Min-heap.
 * Worse case time complexity is O(E), where E is the number of edges in the graph.
 * Read more: [http://theory.stanford.edu/~amitp/GameProgramming/AStarComparison.html]
*/
function AStar(s, e, row, col, inputGrid) {
  const Row = row;
  const Col = col;
  const start = s;
  const end = e;

  function cell() {
    this.cellValue = null;
    this.parent_i = -1;
    this.parent_j = -1;
    this.h = Number.MAX_SAFE_INTEGER;
    this.g = Number.MAX_SAFE_INTEGER;
    this.f = Number.MAX_SAFE_INTEGER;
  }

  function pair(i, j, f) {
    this.i = i;
    this.j = j;
    this.f = f;
  }

  const grid = [];

  for (let i = 0; i < Row; i += 1) {
    grid[i] = [];
    for (let j = 0; j < Col; j += 1) {
      grid[i][j] = new cell();
      grid[i][j].cellValue = inputGrid[i][j];
    }
  }


  const isValid = (i, j) => i >= 0 && j >= 0 && i < Row && j < Col;

  const isDestination = (i, j) => end.i === i && end.j === j;

  const isBlocked = (i, j) => grid[i][j].cellValue === 0;

  const euclideanDistance = (i, j) => Math.abs(i - end.i) * Math.abs(i - end.i)
  + Math.abs(j - end.j) * Math.abs(j - end.j);

  const trace = () => {
    const endRow = end.i;
    const endCol = end.j;
    const startRow = start.i;
    const startCol = start.j;

    let i = endRow;
    let j = endCol;
    const path = [];

    while (!(i === startRow && j === startCol)) {
      path.push([i, j]);

      const nextI = grid[i][j].parent_i;
      const nextJ = grid[i][j].parent_j;
      i = nextI;
      j = nextJ;
    }
    path.push([i, j]);

    for (let i = 0; i < path.length; i += 1) {
      console.log(path[i]);
    }
  };

  const neighbourExplorer = (i, j, parentI, parentJ, openList, openListMap,
    closedListMap, distanceFromParent) => {
    if (!isValid(i, j)) {
      return false;
    }

    if (isBlocked(i, j)) {
      return false;
    }

    if (isDestination(i, j)) {
      grid[i][j].parent_i = parentI;
      grid[i][j].parent_j = parentJ;
      trace();
      return true;
    }


    const g = grid[parentI][parentJ].g + distanceFromParent;
    const h = euclideanDistance(i, j);
    const f = g + h;

    if ((openListMap[[i, j]] && openListMap[[i, j]] > f)
    || (closedListMap[[i, j]] && closedListMap[[i, j]] > f)
    || (!closedListMap[[i, j]] && !openListMap[[i, j]])) {
      openListMap[[i, j]] = f;
      grid[i][j].parent_i = parentI;
      grid[i][j].parent_j = parentJ;
      grid[i][j].g = g;
      grid[i][j].h = h;
      grid[i][j].f = g + h;

      const item = new pair(i, j, f);
      // can be improved by using Min-Heap DataStructure
      if (!openList.length) {
        openList.push(item);
      }
      let k = 0;
      let temp = item;
      for (; k < openList.length; k += 1) {
        if (openList[k].f > item.f) {
          [temp, openList[k]] = [openList[k], temp];
        }
      }
      openList[k] = temp;
    }
    return false;
  };

  const search = () => {
    if (!isValid(start.i, start.j) || !isValid(end.i, end.j)) {
      return false;
    }

    let i = start.i;
    let j = start.j;
    const openList = [];
    const openListMap = new Map();
    const closedListMap = new Map();
    let foundDest = false;

    // Initialize start point
    grid[i][j].h = 0;
    grid[i][j].g = 0;
    grid[i][j].f = 0;

    openList.push(new pair(i, j, 0.0));

    openListMap[[i, j]] = 0;

    while (openList.length > 0) {
      const currentCell = openList.shift();
      i = currentCell.i;
      j = currentCell.j;
      const currentF = currentCell.f;
      closedListMap[[i, j]] = currentF;
      const parentI = i;
      const parentJ = j;

      foundDest = neighbourExplorer(i - 1, j, parentI, parentJ, openList, openListMap, closedListMap, 1); // for North
      if (foundDest) { break; }
      foundDest = neighbourExplorer(i, j - 1, parentI, parentJ, openList, openListMap, closedListMap, 1); // for West
      if (foundDest) { break; }
      foundDest = neighbourExplorer(i + 1, j, parentI, parentJ, openList, openListMap, closedListMap, 1); // for South
      if (foundDest) { break; }
      foundDest = neighbourExplorer(i, j + 1, parentI, parentJ, openList, openListMap, closedListMap, 1); // for East
      if (foundDest) { break; }
      foundDest = neighbourExplorer(i - 1, j - 1, parentI, parentJ, openList, openListMap, closedListMap, 1); // for N.W
      if (foundDest) { break; }
      foundDest = neighbourExplorer(i - 1, j + 1, parentI, parentJ, openList, openListMap, closedListMap, 1);// for S.W
      if (foundDest) { break; }
      foundDest = neighbourExplorer(i + 1, j + 1, parentI, parentJ, openList, openListMap, closedListMap, 1);// for S.E
      if (foundDest) { break; }
      foundDest = neighbourExplorer(i + 1, j - 1, parentI, parentJ, openList, openListMap, closedListMap, 1);// for N.E
      if (foundDest) { break; }
    }


    if (!foundDest) {
      return false;
    }
    return true;
  };
  search();
}


// const inputGrid = [
//   [1, 0, 1, 1, 1, 1, 0, 1, 1, 1],
//   [1, 1, 1, 0, 1, 1, 1, 0, 1, 1],
//   [1, 1, 1, 0, 1, 1, 0, 1, 0, 1],
//   [0, 0, 1, 0, 1, 0, 0, 0, 0, 1],
//   [1, 1, 1, 0, 1, 1, 1, 0, 1, 0],
//   [1, 0, 1, 1, 1, 1, 0, 1, 0, 0],
//   [1, 0, 0, 0, 0, 1, 0, 0, 0, 1],
//   [1, 0, 1, 1, 1, 1, 0, 1, 1, 1],
//   [1, 1, 1, 0, 0, 0, 1, 0, 0, 1],
// ];

const inputGrid = [
  [1, 0, 0, 0, 0, 0],
  [1, 1, 1, 1, 1, 1],
  [1, 0, 0, 0, 0, 0],
  [1, 1, 1, 1, 1, 1],
];

const ROW = inputGrid.length;
const COL = inputGrid[0].length;
const start = {
  i: 0,
  j: 0,
};
const end = {
  i: 3,
  j: 5,
};

AStar(start, end, ROW, COL, inputGrid);


module.exports = {
  AStar,
};