// Copyright 2024 International Digital Economy Academy
//
// 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
//
//     http://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.

///|
/// Trait representing the Writer
pub(open) trait Writer {
  fn write(Self, BytesView) -> Unit raise
}

///|
pub fn[T : Writer] write_varint(writer : T, input : UInt64) -> Unit raise {
  let mut v = input
  while v >= 0x80 {
    writer.write([(v.to_byte() & 0x7F) | 0x80])
    v = v >> 7
  }
  writer.write([v.to_byte()])
}

///|
pub fn[T : Writer] write_tag(writer : T, tag : (UInt, UInt)) -> Unit raise {
  let (tag, wire_type) = tag
  writer |> write_varint(((tag << 3) | wire_type).to_uint64())
}

///|
pub fn[T : Writer] write_int32(writer : T, v : Int) -> Unit raise {
  writer |> write_varint(v.to_uint64())
}

///|
pub fn[T : Writer] write_int64(writer : T, v : Int64) -> Unit raise {
  writer |> write_varint(v.reinterpret_as_uint64())
}

///|
pub fn[T : Writer] write_uint32(writer : T, v : UInt) -> Unit raise {
  writer |> write_varint(v.to_uint64())
}

///|
pub fn[T : Writer] write_uint64(writer : T, v : UInt64) -> Unit raise {
  writer |> write_varint(v)
}

///|
pub fn[T : Writer] write_sint32(writer : T, v : Int) -> Unit raise {
  let v64 = v.to_int64()
  writer |> write_varint(((v64 << 1) ^ (v64 >> 31)).reinterpret_as_uint64())
}

///|
pub fn[T : Writer] write_sint64(writer : T, v : Int64) -> Unit raise {
  writer |> write_varint(((v << 1) ^ (v >> 63)).reinterpret_as_uint64())
}

///|
pub fn[T : Writer] write_fixed32(writer : T, v : UInt) -> Unit raise {
  let mut v = v
  for _ in 0..<4 {
    writer.write([(v & 0xFF) |> UInt::to_byte()])
    v = v >> 8
  }
}

///|
pub fn[T : Writer] write_fixed64(writer : T, v : UInt64) -> Unit raise {
  let mut v = v
  for _ in 0..<8 {
    writer.write([(v & 0xFF) |> UInt64::to_byte()])
    v = v >> 8
  }
}

///|
pub fn[T : Writer] write_sfixed32(writer : T, v : Int) -> Unit raise {
  writer |> write_fixed32(v.reinterpret_as_uint())
}

///|
pub fn[T : Writer] write_sfixed64(writer : T, v : Int64) -> Unit raise {
  writer |> write_fixed64(v.reinterpret_as_uint64())
}

///|
pub fn[T : Writer] write_float(writer : T, v : Float) -> Unit raise {
  writer |> write_fixed32(v.reinterpret_as_uint())
}

///|
pub fn[T : Writer] write_double(writer : T, v : Double) -> Unit raise {
  writer |> write_fixed64(v.reinterpret_as_uint64())
}

///|
pub fn[T : Writer] write_bool(writer : T, v : Bool) -> Unit raise {
  writer.write([if v { 1 } else { 0 }])
}

///|
pub fn[T : Writer] write_enum(writer : T, v : Enum) -> Unit raise {
  writer |> write_varint(v.0.to_uint64())
}

///|
pub fn[T : Writer] write_bytes(writer : T, v : Bytes) -> Unit raise {
  let length = v.length()
  writer |> write_varint(length.to_uint64())
  writer.write(v)
}

///|
pub fn[T : Writer] write_string(writer : T, v : String) -> Unit raise {
  let utf8_string = Buffer()
  for ch in v {
    let ch = ch.to_int()
    if ch < 0x80 {
      utf8_string.write_byte(ch.to_byte())
    } else if ch < 0x800 {
      utf8_string.write_byte((0xC0 | (ch >> 6)).to_byte())
      utf8_string.write_byte((0x80 | (ch & 0x3F)).to_byte())
    } else if ch < 0x10000 {
      utf8_string.write_byte((0xE0 | (ch >> 12)).to_byte())
      utf8_string.write_byte((0x80 | ((ch >> 6) & 0x3F)).to_byte())
      utf8_string.write_byte((0x80 | (ch & 0x3F)).to_byte())
    } else {
      utf8_string.write_byte((0xF0 | (ch >> 18)).to_byte())
      utf8_string.write_byte((0x80 | ((ch >> 12) & 0x3F)).to_byte())
      utf8_string.write_byte((0x80 | ((ch >> 6) & 0x3F)).to_byte())
      utf8_string.write_byte((0x80 | (ch & 0x3F)).to_byte())
    }
  }
  let utf8_string = utf8_string.to_bytes()
  writer |> write_varint(utf8_string.length().reinterpret_as_uint().to_uint64())
  writer.write(utf8_string)
}