function findMaxByteStateChangeCount(messages) {
  return Object.values(messages)
    .map((m) => m.byteStateChangeCounts)
    .reduce((counts, countArr) => counts.concat(countArr), []) // flatten arrays
    .reduce((count1, count2) => (count1 > count2 ? count1 : count2), 0); // find max
}

function addCanMessage(
  canMessage,
  dbc,
  canStartTime,
  messages,
  prevMsgEntries,
  byteStateChangeCountsByMessage
) {
  const {
    address, busTime, data, bus
  } = canMessage;
  const id = `${bus}:${address.toString(16)}`;

  if (messages[id] === undefined) messages[id] = createMessageSpec(dbc, address, id, bus);

  const prevMsgEntry = messages[id].entries.length > 0
    ? messages[id].entries[messages[id].entries.length - 1]
    : prevMsgEntries[id] || null;

  if (
    byteStateChangeCountsByMessage[id]
    && messages[id].byteStateChangeCounts.every((c) => c === 0)
  ) {
    messages[id].byteStateChangeCounts = byteStateChangeCountsByMessage[id];
  }

  const { msgEntry, byteStateChangeCounts } = parseMessage(
    dbc,
    busTime,
    address,
    data,
    canStartTime,
    prevMsgEntry
  );

  messages[id].byteStateChangeCounts = byteStateChangeCounts.map(
    (count, idx) => messages[id].byteStateChangeCounts[idx] + count
  );

  messages[id].entries.push(msgEntry);

  return msgEntry;
}

function createMessageSpec(dbc, address, id, bus) {
  const frame = dbc.getMessageFrame(address);
  const size = frame ? frame.size : 8;

  return {
    address,
    id,
    bus,
    entries: [],
    frame,
    byteColors: Array(size).fill(0),
    byteStateChangeCounts: Array(size).fill(0)
  };
}

function determineByteStateChangeTimes(
  hexData,
  time,
  msgSize,
  lastParsedMessage
) {
  const byteStateChangeCounts = Array(msgSize).fill(0);
  let byteStateChangeTimes;

  if (!lastParsedMessage) {
    byteStateChangeTimes = Array(msgSize).fill(time);
  } else {
    // debugger;
    byteStateChangeTimes = lastParsedMessage.byteStateChangeTimes;

    for (let i = 0; i < byteStateChangeTimes.length; i++) {
      const currentData = hexData.substr(i * 2, 2);
      const prevData = lastParsedMessage.hexData.substr(i * 2, 2);

      if (currentData !== prevData) {
        byteStateChangeTimes[i] = time;
        byteStateChangeCounts[i] = 1;
      }
    }
  }

  return { byteStateChangeTimes, byteStateChangeCounts };
}

function createMessageEntry(
  dbc,
  address,
  time,
  relTime,
  data,
  byteStateChangeTimes
) {
  return {
    signals: dbc.getSignalValues(address, data),
    address,
    data,
    time,
    relTime,
    hexData: Buffer.from(data).toString('hex'),
    byteStateChangeTimes,
    updated: Date.now()
  };
}

function reparseMessage(dbc, msg, lastParsedMessage) {
  const msgSpec = dbc.getMessageFrame(msg.address);
  const msgSize = msgSpec ? msgSpec.size : 8;

  const {
    byteStateChangeTimes,
    byteStateChangeCounts
  } = determineByteStateChangeTimes(
    msg.hexData,
    msg.relTime,
    msgSize,
    lastParsedMessage
  );

  const msgEntry = {
    ...msg,
    signals: dbc.getSignalValues(msg.address, msg.data),
    byteStateChangeTimes,
    updated: Date.now()
  };

  return { msgEntry, byteStateChangeCounts };
}

function parseMessage(dbc, time, address, data, timeStart, lastParsedMessage) {
  let hexData;
  if (typeof data === 'string') {
    hexData = data;
    data = Buffer.from(data, 'hex');
  } else {
    hexData = Buffer.from(data).toString('hex');
  }
  const msgSpec = dbc.getMessageFrame(address);
  const msgSize = msgSpec ? msgSpec.size : 8;
  const relTime = time - timeStart;

  const {
    byteStateChangeTimes,
    byteStateChangeCounts
  } = determineByteStateChangeTimes(
    hexData,
    relTime,
    msgSize,
    lastParsedMessage
  );
  const msgEntry = createMessageEntry(
    dbc,
    address,
    time,
    relTime,
    data,
    byteStateChangeTimes
  );

  return { msgEntry, byteStateChangeCounts };
}

const BIG_ENDIAN_START_BITS = [];
for (let i = 0; i < 64; i += 8) {
  for (let j = 7; j > -1; j--) {
    BIG_ENDIAN_START_BITS.push(i + j);
  }
}

function bigEndianBitIndex(matrixBitIndex) {
  return BIG_ENDIAN_START_BITS.indexOf(matrixBitIndex);
}

function matrixBitNumber(bigEndianIndex) {
  return BIG_ENDIAN_START_BITS[bigEndianIndex];
}

function setMessageByteColors(message, maxByteStateChangeCount) {
  message.byteColors = message.byteStateChangeCounts
    .map((count) => (isNaN(count)
      ? 0
      : Math.min(255, 75 + 180 * (count / maxByteStateChangeCount))))
    .map((red) => `rgb(${Math.round(red)},0,0)`);

  return message;
}

export default {
  bigEndianBitIndex,
  addCanMessage,
  createMessageSpec,
  matrixBitNumber,
  parseMessage,
  reparseMessage,
  findMaxByteStateChangeCount,
  setMessageByteColors,
  createMessageEntry
};
