// OBD-II (SAE J1979) mode 01/03/07/0A decoding.

pub(all) enum ObdMessage {
  Mode01(Int, Array[Int])
  DtcResponse(Int, Array[Dtc])
  Unknown(Array[Int])
}

pub(all) struct ObdReading {
  pid : Int
  label : String
  value : String
  unit : String
  raw : Array[Int]
}

fn fmt_1(value : Double) -> String {
  let negative = value < 0.0
  let scaled = (value.abs() * 10.0).round().to_int()
  let whole = scaled / 10
  let frac = scaled % 10
  (if negative { "-" } else { "" }) + whole.to_string() + "." + frac.to_string()
}

fn fmt_2(value : Double) -> String {
  let negative = value < 0.0
  let scaled = (value.abs() * 100.0).round().to_int()
  let whole = scaled / 100
  let frac = scaled % 100
  let body = if frac < 10 {
    whole.to_string() + ".0" + frac.to_string()
  } else {
    whole.to_string() + "." + frac.to_string()
  }
  (if negative { "-" } else { "" }) + body
}

fn byte_at(data : Array[Int], index : Int) -> Int {
  if index >= 0 && index < data.length() {
    data[index]
  } else {
    0
  }
}

fn supported_pids(data : Array[Int]) -> String {
  let out = StringBuilder()
  for i in 0..<4 {
    let byte = byte_at(data, i)
    for bit in 0..<8 {
      if ((byte >> (7 - bit)) & 1) == 1 {
        let pid = i * 8 + bit + 1
        if out.to_string().length() > 0 {
          out.write_char(' ')
        }
        out.write_string("0x" + pid.to_string())
      }
    }
  }
  out.to_string()
}

fn fuel_type(code : Int) -> String {
  if code == 1 {
    "Gasoline"
  } else if code == 2 {
    "Methanol"
  } else if code == 4 {
    "Ethanol"
  } else if code == 8 {
    "Diesel"
  } else if code == 16 {
    "LPG"
  } else if code == 32 {
    "CNG"
  } else if code == 64 {
    "Electric"
  } else {
    "Unknown(" + code.to_string() + ")"
  }
}

fn monitor_bits(byte : Int, names : Array[String]) -> String {
  let out = StringBuilder()
  for i in 0..<8 {
    if ((byte >> i) & 1) == 1 {
      if out.to_string().length() > 0 {
        out.write_char(',')
      }
      out.write_string(names[i])
    }
  }
  out.to_string()
}

pub fn decode_obd_pid(pid : Int, data : Array[Int]) -> ObdReading? {
  let a = byte_at(data, 0)
  let b = byte_at(data, 1)
  let reading = if pid == 0x00 || pid == 0x20 || pid == 0x40 || pid == 0x60 {
    Some({
      pid,
      label: "Supported PIDs",
      value: supported_pids(data),
      unit: "",
      raw: data,
    })
  } else if pid == 0x01 {
    let mil = (a & 0x80) != 0
    let count = a & 0x7f
    Some({
      pid,
      label: "Monitor status",
      value: "MIL=" + mil.to_string() + " DTCs=" + count.to_string(),
      unit: "",
      raw: data,
    })
  } else if pid == 0x03 {
    Some({
      pid,
      label: "Fuel system status",
      value: "0x" + a.to_string() + " 0x" + b.to_string(),
      unit: "",
      raw: data,
    })
  } else if pid == 0x04 {
    Some({ pid, label: "Calculated engine load", value: fmt_1(a.to_double() * 100.0 / 255.0), unit: "%", raw: data })
  } else if pid == 0x05 {
    Some({ pid, label: "Coolant temperature", value: (a - 40).to_string(), unit: "C", raw: data })
  } else if pid == 0x06 || pid == 0x07 || pid == 0x08 || pid == 0x09 {
    let label = if pid == 0x06 {
      "Short term fuel trim bank 1"
    } else if pid == 0x07 {
      "Long term fuel trim bank 1"
    } else if pid == 0x08 {
      "Short term fuel trim bank 2"
    } else {
      "Long term fuel trim bank 2"
    }
    Some({ pid, label, value: fmt_1((a.to_double() - 128.0) * 100.0 / 128.0), unit: "%", raw: data })
  } else if pid == 0x0a {
    Some({ pid, label: "Fuel pressure", value: (a * 3).to_string(), unit: "kPa", raw: data })
  } else if pid == 0x0b {
    Some({ pid, label: "Intake manifold pressure", value: a.to_string(), unit: "kPa", raw: data })
  } else if pid == 0x0c {
    Some({ pid, label: "Engine speed", value: ((a * 256 + b) / 4).to_string(), unit: "rpm", raw: data })
  } else if pid == 0x0d {
    Some({ pid, label: "Vehicle speed", value: a.to_string(), unit: "km/h", raw: data })
  } else if pid == 0x0e {
    Some({ pid, label: "Timing advance", value: fmt_1(a.to_double() / 2.0 - 64.0), unit: "deg", raw: data })
  } else if pid == 0x0f {
    Some({ pid, label: "Intake air temperature", value: (a - 40).to_string(), unit: "C", raw: data })
  } else if pid == 0x10 {
    Some({ pid, label: "MAF air flow", value: fmt_2((a * 256 + b).to_double() / 100.0), unit: "g/s", raw: data })
  } else if pid == 0x11 {
    Some({ pid, label: "Throttle position", value: fmt_1(a.to_double() * 100.0 / 255.0), unit: "%", raw: data })
  } else if pid == 0x1f {
    Some({ pid, label: "Run time since start", value: (a * 256 + b).to_string(), unit: "s", raw: data })
  } else if pid == 0x21 {
    Some({ pid, label: "Distance with MIL on", value: (a * 256 + b).to_string(), unit: "km", raw: data })
  } else if pid == 0x2f {
    Some({ pid, label: "Fuel level", value: fmt_1(a.to_double() * 100.0 / 255.0), unit: "%", raw: data })
  } else if pid == 0x31 {
    Some({ pid, label: "Distance since codes cleared", value: (a * 256 + b).to_string(), unit: "km", raw: data })
  } else if pid == 0x33 {
    Some({ pid, label: "Barometric pressure", value: a.to_string(), unit: "kPa", raw: data })
  } else if pid == 0x42 {
    Some({ pid, label: "Control module voltage", value: fmt_2((a * 256 + b).to_double() / 1000.0), unit: "V", raw: data })
  } else if pid == 0x46 {
    Some({ pid, label: "Ambient air temperature", value: (a - 40).to_string(), unit: "C", raw: data })
  } else if pid == 0x51 {
    Some({ pid, label: "Fuel type", value: fuel_type(a), unit: "", raw: data })
  } else if pid == 0x5c {
    Some({ pid, label: "Engine oil temperature", value: (a - 40).to_string(), unit: "C", raw: data })
  } else {
    None
  }
  reading
}

pub fn readiness_summary(data : Array[Int]) -> String {
  let a = byte_at(data, 0)
  let b = byte_at(data, 1)
  let c = byte_at(data, 2)
  let d = byte_at(data, 3)
  let since = monitor_bits(b, [
    "misfire", "fuelSystem", "components", "catalyst", "heatedCatalyst",
    "evaporativeSystem", "secondaryAir", "acRefrigerant",
  ])
  let available = monitor_bits(d, [
    "oxygenSensor", "oxygenSensorHeater", "egr", "reserved4", "reserved5",
    "reserved6", "reserved7", "reserved8",
  ])
  "MIL=" + ((a & 0x80) != 0).to_string() + " dtcCount=" +
  (a & 0x7f).to_string() + " incomplete=" + since + " available=" + available +
  " encoded=0x" + c.to_string() + "0x" + d.to_string()
}

pub fn parse_obd_message(bytes : Array[Int]) -> ObdMessage {
  if bytes.length() < 2 {
    return Unknown(bytes)
  }
  if bytes[0] == 0x41 {
    Mode01(bytes[1], bytes)
  } else if bytes[0] == 0x43 || bytes[0] == 0x47 || bytes[0] == 0x4a {
    let dtcs : Array[Dtc] = Array::new()
    let mut i = 2
    while i + 1 < bytes.length() {
      if is_present_dtc(bytes[i], bytes[i + 1]) {
        dtcs.push(decode_dtc(bytes[i], bytes[i + 1]))
      }
      i = i + 2
    }
    DtcResponse(bytes[0], dtcs)
  } else {
    Unknown(bytes)
  }
}

// OBD mode 09 PID 02 returns the VIN. Some ECUs prefix a frame counter, so
// take the final 17 printable ASCII bytes after the mode/PID header.
pub fn parse_obd_vin(bytes : Array[Int]) -> String? {
  if bytes.length() < 19 || bytes[0] != 0x49 || bytes[1] != 0x02 {
    return None
  }
  let start = bytes.length() - 17
  let vin = StringBuilder()
  for i in start.. 126 {
      return None
    }
    vin.write_char(Char::from_int(b))
  }
  Some(vin.to_string())
}

pub(all) struct MonitorResult {
  mid : Int
  tid : Int
  unit : Int
  value : Int
}

// Mode 06: 46 MID TID UAS TestValue(2) Min(2) Max(2) ...
pub fn parse_obd_mode06(bytes : Array[Int]) -> MonitorResult? {
  if bytes.length() < 8 || bytes[0] != 0x46 {
    return None
  }
  Some({
    mid: bytes[1],
    tid: bytes[2],
    unit: bytes[3],
    value: (bytes[4] << 8) | bytes[5],
  })
}