/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */

import { Scope } from "./scope";
import TypeResolver from "../typeResolver";

export class BinaryReaderBuilder {
  constructor(private holder: string) {
  }

  ownName() {
    return this.holder;
  }

  readGetCursor() {
    return `${this.holder}.readGetCursor()`;
  }

  readSetCursor(v: number | string) {
    return `${this.holder}.readSetCursor(${v})`;
  }

  readVarInt32() {
    return `${this.holder}.readVarInt32()`;
  }

  readTaggedInt64() {
    return `${this.holder}.readTaggedInt64()`;
  }

  readTaggedUInt64() {
    return `${this.holder}.readTaggedUInt64()`;
  }

  readVarInt64() {
    return `${this.holder}.readVarInt64()`;
  }

  readVarUInt32() {
    return `${this.holder}.readVarUInt32()`;
  }

  readVarUInt64() {
    return `${this.holder}.readVarUInt64()`;
  }

  readInt8() {
    return `${this.holder}.readInt8()`;
  }

  buffer(len: string | number) {
    return `${this.holder}.buffer(${len})`;
  }

  bufferRef(len: string | number) {
    return `${this.holder}.bufferRef(${len})`;
  }

  readUint8() {
    return `${this.holder}.readUint8()`;
  }

  stringUtf8At() {
    return `${this.holder}.stringUtf8At()`;
  }

  stringUtf8() {
    return `${this.holder}.stringUtf8()`;
  }

  stringLatin1() {
    return `${this.holder}.stringLatin1()`;
  }

  stringWithHeader() {
    return `${this.holder}.stringWithHeader()`;
  }

  readFloat64() {
    return `${this.holder}.readFloat64()`;
  }

  readFloat32() {
    return `${this.holder}.readFloat32()`;
  }

  readFloat16() {
    return `${this.holder}.readFloat16()`;
  }

  readBfloat16() {
    return `${this.holder}.readBfloat16()`;
  }

  readUint16() {
    return `${this.holder}.readUint16()`;
  }

  readInt16() {
    return `${this.holder}.readInt16()`;
  }

  readVarUint32Small7() {
    return `${this.holder}.readVarUint32Small7()`;
  }

  readUint64() {
    return `${this.holder}.readUint64()`;
  }

  readSkip(v: number) {
    return `${this.holder}.readSkip(${v})`;
  }

  readInt64() {
    return `${this.holder}.readInt64()`;
  }

  readSliInt64() {
    return `${this.holder}.readSliInt64()`;
  }

  readUint32() {
    return `${this.holder}.readUint32()`;
  }

  readInt32() {
    return `${this.holder}.readInt32()`;
  }
}

class BinaryWriterBuilder {
  constructor(private holder: string) {
  }

  ownName() {
    return this.holder;
  }

  writeSkip(v: number | string) {
    return `${this.holder}.writeSkip(${v})`;
  }

  getByteLen() {
    return `${this.holder}.getByteLen()`;
  }

  getReserved() {
    return `${this.holder}.getReserved()`;
  }

  reserve(v: number | string) {
    return `${this.holder}.reserve(${v})`;
  }

  arrayBuffer(buffer: number | string, byteOffset: number | string, byteLength: number | string) {
    return `${this.holder}.arrayBuffer(${buffer}, ${byteOffset}, ${byteLength})`;
  }

  writeUint16(v: number | string) {
    return `${this.holder}.writeUint16(${v})`;
  }

  writeInt8(v: number | string) {
    return `${this.holder}.writeInt8(${v})`;
  }

  writeInt24(v: number | string) {
    return `${this.holder}.writeInt24(${v})`;
  }

  writeUint8(v: number | string) {
    return `${this.holder}.writeUint8(${v})`;
  }

  writeInt16(v: number | string) {
    return `${this.holder}.writeInt16(${v})`;
  }

  writeVarInt32(v: number | string) {
    return `${this.holder}.writeVarInt32(${v})`;
  }

  writeVarUint32Small7(v: number | string) {
    return `${this.holder}.writeVarUint32Small7(${v})`;
  }

  writeVarUInt32(v: number | string) {
    return `${this.holder}.writeVarUInt32(${v})`;
  }

  writeVarUInt64(v: number | string) {
    return `${this.holder}.writeVarUInt64(${v})`;
  }

  writeTaggedInt64(v: number | string) {
    return `${this.holder}.writeTaggedInt64(${v})`;
  }

  writeTaggedUInt64(v: number | string) {
    return `${this.holder}.writeTaggedUInt64(${v})`;
  }

  writeVarInt64(v: number | string) {
    return `${this.holder}.writeVarInt64(${v})`;
  }

  stringWithHeader(str: string) {
    return `${this.holder}.stringWithHeader(${str})`;
  }

  bufferWithoutMemCheck(v: string) {
    return `${this.holder}.bufferWithoutMemCheck(${v})`;
  }

  writeUint64(v: number | string) {
    return `${this.holder}.writeUint64(${v})`;
  }

  buffer(v: string) {
    return `${this.holder}.buffer(${v})`;
  }

  writeFloat64(v: number | string) {
    return `${this.holder}.writeFloat64(${v})`;
  }

  writeFloat32(v: number | string) {
    return `${this.holder}.writeFloat32(${v})`;
  }

  writeFloat16(v: number | string) {
    return `${this.holder}.writeFloat16(${v})`;
  }

  writeBfloat16(v: number | string) {
    return `${this.holder}.writeBfloat16(${v})`;
  }

  writeInt64(v: number | string) {
    return `${this.holder}.writeInt64(${v})`;
  }

  writeSliInt64(v: number | string) {
    return `${this.holder}.writeSliInt64(${v})`;
  }

  writeUint32(v: number | string) {
    return `${this.holder}.writeUint32(${v})`;
  }

  writeInt32(v: number | string) {
    return `${this.holder}.writeInt32(${v})`;
  }

  writeGetCursor() {
    return `${this.holder}.writeGetCursor()`;
  }

  setUint32Position(offset: number | string, v: number | string) {
    return `${this.holder}.setUint32Position(${offset}, ${v})`;
  }

  setUint8Position(offset: number | string, v: number | string) {
    return `${this.holder}.setUint8Position(${offset}, ${v})`;
  }

  setUint16Position(offset: number | string, v: number | string) {
    return `${this.holder}.setUint16Position(${offset}, ${v})`;
  }
}

class ReferenceResolverBuilder {
  constructor(private readHolder: string, private writeHolder: string) {
  }

  ownReadName() {
    return this.readHolder;
  }

  ownWriteName() {
    return this.writeHolder;
  }

  getReadRef(id: string | number) {
    return `${this.readHolder}.getReadRef(${id})`;
  }

  readRefFlag() {
    return `${this.readHolder}.readRefFlag()`;
  }

  reference(obj: string) {
    return `${this.readHolder}.reference(${obj})`;
  }

  writeRef(obj: string) {
    return `${this.writeHolder}.writeRef(${obj})`;
  }

  getWrittenRefId(obj: string) {
    return `${this.writeHolder}.getWrittenRefId(${obj})`;
  }
}

class TypeResolverBuilder {
  constructor(private holder: string) {
  }

  ownName() {
    return this.holder;
  }

  getSerializerById(id: string | number, userTypeId?: string | number) {
    if (userTypeId === undefined) {
      return `${this.holder}.getSerializerById(${id})`;
    }
    return `${this.holder}.getSerializerById(${id}, ${userTypeId})`;
  }

  getSerializerByName(name: string) {
    return `${this.holder}.getSerializerByName("${name}")`;
  }

  getSerializerByData(v: string) {
    return `${this.holder}.getSerializerByData(${v})`;
  }
}

class TypeMetaContextBuilder {
  constructor(private writeHolder: string, private readHolder: string) {
  }

  writeTypeMeta(typeInfo: string, bytes: string) {
    return `${this.writeHolder}.writeTypeMeta(${typeInfo}, ${bytes})`;
  }

  readTypeMeta() {
    return `${this.readHolder}.readTypeMeta()`;
  }

  genSerializerByTypeMetaRuntime(typeMeta: string, original?: string) {
    if (original) {
      return `${this.readHolder}.genSerializerByTypeMetaRuntime(${typeMeta}, ${original})`;
    }
    return `${this.readHolder}.genSerializerByTypeMetaRuntime(${typeMeta})`;
  }
}

class MetaStringContextBuilder {
  constructor(
    private writeContextHolder: string,
    private readContextHolder: string,
    private writeHelperHolder: string,
  ) {
  }

  writeBytes(bytes: string) {
    return `${this.writeContextHolder}.writeMetaStringBytes(${bytes})`;
  }

  readTypeName() {
    return `${this.readContextHolder}.readTypeName()`;
  }

  readNamespace() {
    return `${this.readContextHolder}.readNamespace()`;
  }

  encodeNamespace(input: string) {
    return `${this.writeHelperHolder}.encodeNamespace("${input}")`;
  }

  encodeTypeName(input: string) {
    return `${this.writeHelperHolder}.encodeTypeName("${input}")`;
  }
}

export class CodecBuilder {
  readonly reader: BinaryReaderBuilder;
  readonly writer: BinaryWriterBuilder;
  readonly referenceResolver: ReferenceResolverBuilder;
  readonly typeResolver: TypeResolverBuilder;
  readonly typeMetaResolver: TypeMetaContextBuilder;
  readonly metaStringResolver: MetaStringContextBuilder;

  constructor(scope: Scope, readonly resolver: TypeResolver) {
    const writeContext = scope.declareByName("writeContext", "typeResolver.writeContext");
    const readContext = scope.declareByName("readContext", "typeResolver.readContext");
    const br = scope.declareByName("br", "readContext.reader");
    const bw = scope.declareByName("bw", "writeContext.writer");
    const cr = scope.declareByName("cr", "typeResolver");
    const rw = scope.declareByName("rw", "writeContext.refWriter");
    const rr = scope.declareByName("rr", "readContext.refReader");
    const mw = scope.declareByName("mw", "writeContext.metaStringWriter");
    scope.declareByName("mr", "readContext.metaStringReader");
    this.reader = new BinaryReaderBuilder(br);
    this.writer = new BinaryWriterBuilder(bw);
    this.typeResolver = new TypeResolverBuilder(cr);
    this.referenceResolver = new ReferenceResolverBuilder(rr, rw);
    this.typeMetaResolver = new TypeMetaContextBuilder(writeContext, readContext);
    this.metaStringResolver = new MetaStringContextBuilder(writeContext, readContext, mw);
  }

  static isReserved(key: string) {
    return /^(?:do|if|in|for|let|new|try|var|case|else|enum|eval|false|null|this|true|void|with|break|catch|class|const|super|throw|while|yield|delete|export|import|public|return|static|switch|typeof|default|extends|finally|package|private|continue|debugger|function|arguments|interface|protected|implements|instanceof)$/.test(key);
  }

  static isDotPropAccessor(prop: string) {
    return /^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(prop);
  }

  static replaceBackslashAndQuote(v: string) {
    return v.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
  }

  static safeString(target: string) {
    if (!CodecBuilder.isDotPropAccessor(target) || CodecBuilder.isReserved(target)) {
      return `"${CodecBuilder.replaceBackslashAndQuote(target)}"`;
    }
    return `"${target}"`;
  }

  static safePropAccessor(prop: string) {
    if (!CodecBuilder.isDotPropAccessor(prop) || CodecBuilder.isReserved(prop)) {
      return `["${CodecBuilder.replaceBackslashAndQuote(prop)}"]`;
    }
    return `.${prop}`;
  }

  static safePropName(prop: string) {
    if (!CodecBuilder.isDotPropAccessor(prop) || CodecBuilder.isReserved(prop)) {
      return `["${CodecBuilder.replaceBackslashAndQuote(prop)}"]`;
    }
    return prop;
  }

  getTypeResolverName() {
    return "typeResolver";
  }

  getWriteContextName() {
    return "writeContext";
  }

  getReadContextName() {
    return "readContext";
  }

  getExternal(key: string) {
    return `external.${key}`;
  }

  getOptions(key: string) {
    return `options.${key}`;
  }

  getTypeInfo() {
    return "typeInfo";
  }
}
