// Copyright 2021 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.

/** Decoder class for decoding bytes using HDLC protocol */

import * as protocol from './protocol';
import * as util from './util';

const _MIN_FRAME_SIZE = 6; // 1 B address + 1 B control + 4 B CRC-32

/** Indicates if an error occurred */
export enum FrameStatus {
  OK = 'OK',
  FCS_MISMATCH = 'frame check sequence failure',
  FRAMING_ERROR = 'invalid flag or escape characters',
  BAD_ADDRESS = 'address field too long',
}

/**
 * A single HDLC frame
 */
export class Frame {
  rawEncoded: Uint8Array;
  rawDecoded: Uint8Array;
  status: FrameStatus;

  address = -1;
  control: Uint8Array = new Uint8Array(0);
  data: Uint8Array = new Uint8Array(0);

  constructor(
    rawEncoded: Uint8Array,
    rawDecoded: Uint8Array,
    status: FrameStatus = FrameStatus.OK,
  ) {
    this.rawEncoded = rawEncoded;
    this.rawDecoded = rawDecoded;
    this.status = status;

    if (status === FrameStatus.OK) {
      const [address, addressLength] = protocol.decodeAddress(rawDecoded);
      if (addressLength === 0) {
        this.status = FrameStatus.BAD_ADDRESS;
        return;
      }
      this.address = address;
      this.control = rawDecoded.slice(addressLength, addressLength + 1);
      this.data = rawDecoded.slice(addressLength + 1, -4);
    }
  }

  /**
   * True if this represents a valid frame.
   *
   * If false, then parsing failed. The status is set to indicate what type of
   * error occurred, and the data field contains all bytes parsed from the frame
   * (including bytes parsed as address or control bytes).
   */
  ok(): boolean {
    return this.status === FrameStatus.OK;
  }
}

enum DecoderState {
  INTERFRAME,
  FRAME,
  FRAME_ESCAPE,
}

/** Decodes one or more HDLC frames from a stream of data. */
export class Decoder {
  private decodedData = new Uint8Array(0);
  private rawData = new Uint8Array(0);
  private state = DecoderState.INTERFRAME;

  /**
   *  Decodes and yields HDLC frames, including corrupt frames
   *
   *  The ok() method on Frame indicates whether it is valid or represents a
   *  frame parsing error.
   *
   *  @yield Frames, which may be valid (frame.ok)) okr corrupt (!frame.ok())
   */
  *process(data: Uint8Array): IterableIterator<Frame> {
    for (const byte of data) {
      const frame = this.processByte(byte);
      if (frame != null) {
        yield frame;
      }
    }
  }

  /**
   *  Decodes and yields valid HDLC frames, logging any errors.
   *
   *  @yield Valid HDLC frames
   */
  *processValidFrames(data: Uint8Array): IterableIterator<Frame> {
    const frames = this.process(data);
    for (const frame of frames) {
      if (frame.ok()) {
        yield frame;
      } else {
        console.warn(
          'Failed to decode frame: %s; discarded %d bytes',
          frame.status,
          frame.rawEncoded.length,
        );
        console.debug('Discarded data: %s', frame.rawEncoded);
      }
    }
  }

  private checkFrame(data: Uint8Array): FrameStatus {
    if (data.length < _MIN_FRAME_SIZE) {
      return FrameStatus.FRAMING_ERROR;
    }
    const frameCrc = new DataView(data.slice(-4).buffer).getInt8(0);
    const crc = new DataView(
      protocol.frameCheckSequence(data.slice(0, -4)).buffer,
    ).getInt8(0);
    if (crc !== frameCrc) {
      return FrameStatus.FCS_MISMATCH;
    }
    return FrameStatus.OK;
  }

  private finishFrame(status: FrameStatus): Frame {
    const frame = new Frame(
      new Uint8Array(this.rawData),
      new Uint8Array(this.decodedData),
      status,
    );
    this.rawData = new Uint8Array(0);
    this.decodedData = new Uint8Array(0);
    return frame;
  }

  private processByte(byte: number): Frame | undefined {
    let frame;

    // Record every byte except the flag character.
    if (byte != protocol.FLAG) {
      this.rawData = util.concatenate(this.rawData, Uint8Array.of(byte));
    }

    switch (this.state) {
      case DecoderState.INTERFRAME:
        if (byte === protocol.FLAG) {
          if (this.rawData.length > 0) {
            frame = this.finishFrame(FrameStatus.FRAMING_ERROR);
          }
          this.state = DecoderState.FRAME;
        }
        break;

      case DecoderState.FRAME:
        if (byte == protocol.FLAG) {
          if (this.rawData.length > 0) {
            frame = this.finishFrame(this.checkFrame(this.decodedData));
          }
        } else if (byte == protocol.ESCAPE) {
          this.state = DecoderState.FRAME_ESCAPE;
        } else {
          this.decodedData = util.concatenate(
            this.decodedData,
            Uint8Array.of(byte),
          );
        }
        break;

      case DecoderState.FRAME_ESCAPE:
        if (byte === protocol.FLAG) {
          frame = this.finishFrame(FrameStatus.FRAMING_ERROR);
          this.state = DecoderState.FRAME;
        } else if (protocol.VALID_ESCAPED_BYTES.includes(byte)) {
          this.state = DecoderState.FRAME;
          this.decodedData = util.concatenate(
            this.decodedData,
            Uint8Array.of(protocol.escape(byte)),
          );
        } else {
          this.state = DecoderState.INTERFRAME;
        }
        break;
    }
    return frame;
  }
}
