// Copyright (c) 2022, 2024, Oracle and/or its affiliates.

//-----------------------------------------------------------------------------
//
// This software is dual-licensed to you under the Universal Permissive License
// (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License
// 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose
// either license.
//
// If you elect to accept the software under the Apache License, Version 2.0,
// the following applies:
//
// 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.
//
//-----------------------------------------------------------------------------

'use strict';

const { Buffer } = require('buffer');
const constants = require("../constants.js");
const errors = require("../../../errors.js");

/**
 *
 * Base class for all the RPC messages to support encode/decode functions
 */
class Message {
  constructor(connection) {
    this.errorInfo = {};
    this.connection = connection;
    this.messageType = constants.TNS_MSG_TYPE_FUNCTION;
    this.functionCode = 0;
    this.callStatus = 0;
    this.flushOutBinds = false;
    this.endOfResponse = false;
    this.endToEndSeqNum = 0;
    this.errorOccurred = false;
    this.warning = undefined;
  }

  preProcess() { }
  async postProcess() { }

  writeFunctionHeader(buf) {
    buf.writeUInt8(this.messageType);
    buf.writeUInt8(this.functionCode);
    buf.writeSeqNum();
    if (buf.caps.ttcFieldVersion >= constants.TNS_CCAP_FIELD_VERSION_23_1_EXT_1) {
      buf.writeUB8(0);                          // token number
    }
  }

  processErrorInfo(buf) {
    this.callStatus = buf.readUB4();            // end of call status
    buf.skipUB2();                              // end to end seq number
    buf.skipUB4();                              // current row number
    buf.skipUB2();                              // error number
    buf.skipUB2();                              // array elem error
    buf.skipUB2();                              // array elem error
    this.errorInfo.cursorId = buf.readUB2();    // cursor id
    const errorPos = buf.readSB2();             // error position
    buf.skipUB1();                              // sql type (19c and earlier)
    buf.skipUB1();                              // fatal ?
    buf.skipUB1();                              // flags
    buf.skipUB1();                              // user cursor options
    buf.skipUB1();                              // UPI parameter
    const warnFlag = buf.readUInt8();           // warning flag
    if (warnFlag & constants.TNS_WARN_COMPILATION_CREATE) {
      this.warning = errors.getErr(errors.WRN_COMPILATION_CREATE);
    }
    this.errorInfo.rowID = buf.readRowID();     // rowid
    buf.skipUB4();                              // OS error
    buf.skipUB1();                              // statement error
    buf.skipUB1();                              // call number
    buf.skipUB2();                              // padding
    buf.skipUB4();                              // success iters
    const numBytes = buf.readUB4();               // oerrdd (logical rowid)
    if (numBytes > 0) {
      buf.skipBytesChunked();
    }
    // batch error codes
    const numErrors = buf.readUB2();             // batch error codes array
    if (numErrors > 0) {
      this.errorInfo.batchErrors = [];
      const firstByte = buf.readUInt8();
      for (let i = 0; i < numErrors; i++) {
        if (firstByte === constants.TNS_LONG_LENGTH_INDICATOR) {
          buf.skipUB4();                        // chunk length ignored
        }
        const errorCode = buf.readUB2();
        this.errorInfo.batchErrors.push(new Error(errorCode));
      }
      if (firstByte === constants.TNS_LONG_LENGTH_INDICATOR) {
        buf.skipBytes(1);                       // ignore end marker
      }
    }

    // batch error offset
    const numOffsets = buf.readUB4();           // batch error row offset array
    if (numOffsets > 0) {
      if (numOffsets > 65535) {
        errors.throwErr(errors.ERR_TOO_MANY_BATCH_ERRORS);
      }
      const firstByte = buf.readUInt8();
      let offset;
      for (let i = 0; i < numOffsets; i++) {
        if (firstByte === constants.TNS_LONG_LENGTH_INDICATOR) {
          buf.skipUB4();                        // chunk length ignored
        }
        offset = buf.readUB4();
        if (i < numErrors) {
          this.errorInfo.batchErrors[i].offset = offset;
        }
      }
      if (firstByte === constants.TNS_LONG_LENGTH_INDICATOR) {
        buf.skipBytes(1);                       // ignore end marker
      }
    }

    // batch error messages
    const errMsgArr = buf.readUB2();                 // batch error messages array
    if (errMsgArr > 0) {
      buf.skipBytes(1);                         // ignore packed size
      for (let i = 0; i < errMsgArr; i++) {
        buf.skipUB2();                          // skip chunk length

        this.errorInfo.batchErrors[i].message = buf.readStr(constants.CSFRM_IMPLICIT);
        buf.skipBytes(2);                       // ignore end marker
      }
    }

    this.errorInfo.num = buf.readUB4();         // error number (extended)
    this.errorInfo.rowCount = buf.readUB8();    // row number (extended)

    // fields added in Oracle Database 20c
    if (buf.caps.ttcFieldVersion >= constants.TNS_CCAP_FIELD_VERSION_20_1) {
      buf.skipUB4();                           // sql type
      buf.skipUB4();                           // server checksum
    }

    // error message
    if (this.errorInfo.num !== 0) {
      this.errorOccurred = true;
      if (errorPos >= 0) {
        this.errorInfo.pos = errorPos;
      }
      this.errorInfo.message = buf.readStr(constants.CSFRM_IMPLICIT);
      /*
       * Remove ending newline from ORA error message
       */
      this.errorInfo.message = this.errorInfo.message.trim();
    }
    this.endOfResponse = !this.connection.nscon.endOfRequestSupport;
  }

  processReturnParameter() { }

  processWarningInfo(buf) {
    const errNum = buf.readUB2();              // warning number
    const numBytes = buf.readUB2();            // length of warning message
    buf.skipUB2();                             // flags
    if (errNum != 0 && numBytes > 0) {
      // get message string and remove the ending newline.
      const message = buf.readStr(constants.CSFRM_IMPLICIT).trim();
      this.warning = new Error(message);
      this.warning.errorNum = errNum;
    }
  }

  decode(buf) {
    this.process(buf);
  }

  process(buf) {
    this.endOfResponse = false;
    this.flushOutBinds = false;
    do {
      this.savePoint(buf);
      const messageType = buf.readUInt8();
      this.processMessage(buf, messageType);
    } while (!this.endOfResponse);
  }

  savePoint(buf) {
    buf.savePoint();
  }

  processMessage(buf, messageType) {
    if (messageType === constants.TNS_MSG_TYPE_ERROR) {
      this.processErrorInfo(buf);
    } else if (messageType === constants.TNS_MSG_TYPE_WARNING) {
      this.processWarningInfo(buf);
    } else if (messageType === constants.TNS_MSG_TYPE_STATUS) {
      this.callStatus = buf.readUB4();
      this.endToEndSeqNum = buf.readUB2();
      this.endOfResponse = !this.connection.nscon.endOfRequestSupport;
    } else if (messageType === constants.TNS_MSG_TYPE_PARAMETER) {
      this.processReturnParameter(buf);
    } else if (messageType === constants.TNS_MSG_TYPE_SERVER_SIDE_PIGGYBACK) {
      this.processServerSidePiggyBack(buf);
    } else if (messageType === constants.TNS_MSG_TYPE_END_OF_REQUEST) {
      this.endOfResponse = true;
    } else {
      errors.throwErr(errors.ERR_UNEXPECTED_MESSAGE_TYPE, messageType, buf.pos, buf.packetNum);
    }
  }

  processServerSidePiggyBack(buf) {
    const opcode = buf.readUInt8();
    if (opcode === constants.TNS_SERVER_PIGGYBACK_LTXID) {
      const num_bytes = buf.readUB4();
      if (num_bytes > 0) {
        buf.skipBytesChunked();
      }
    } else if ((opcode === constants.TNS_SERVER_PIGGYBACK_QUERY_CACHE_INVALIDATION)
     || (opcode === constants.TNS_SERVER_PIGGYBACK_TRACE_EVENT)) {
      // pass
    } else if (opcode === constants.TNS_SERVER_PIGGYBACK_OS_PID_MTS) {
      const numDtys = buf.readUB2();
      buf.skipUB1();
      buf.skipBytes(numDtys);
    } else if (opcode === constants.TNS_SERVER_PIGGYBACK_SYNC) {
      buf.skipUB2();                            // skip number of DTYs
      buf.skipUB1();                            // skip length of DTYs
      const num_elements = buf.readUB4();
      buf.skipBytes(1);                         // skip length
      for (let i = 0; i < num_elements; i++) {
        let temp16 = buf.readUB2();
        if (temp16 > 0) {                       // skip key
          buf.skipBytesChunked();
        }
        temp16 = buf.readUB2();
        if (temp16 > 0) {                       // skip value
          buf.skipBytesChunked();
        }
        buf.skipUB2();                          // skip flags
      }
      buf.skipUB4();                            // skip overall flags
    } else if (opcode === constants.TNS_SERVER_PIGGYBACK_EXT_SYNC) {
      buf.skipUB2();
      buf.skipUB1();
    } else if (opcode === constants.TNS_SERVER_PIGGYBACK_AC_REPLAY_CONTEXT) {
      buf.skipUB2();                            // skip number of DTYs
      buf.skipUB1();                            // skip length of DTYs
      buf.skipUB4();                            // skip flags
      buf.skipUB4();                            // skip error code
      buf.skipUB1();                            // skip queue
      const num_bytes = buf.readUB4();          // skip replay context
      if (num_bytes > 0) {
        buf.skipBytesChunked();
      }
    } else if (opcode === constants.TNS_SERVER_PIGGYBACK_SESS_RET) {
      buf.skipUB2();
      buf.skipUB1();
      const num_elements = buf.readUB2();
      if (num_elements > 0) {
        buf.skipUB1();
        for (let i = 0; i < num_elements; ++i) {
          let temp16 = buf.readUB2();
          if (temp16 > 0) {                     // skip key
            buf.skipBytesChunked();
          }
          temp16 = buf.readUB2();
          if (temp16 > 0) {                     // skip value
            buf.skipBytesChunked();
          }
          buf.skipUB2();                        // skip flags
        }
      }
      const flags = buf.readUB4();              // session flags
      if (flags & constants.TNS_SESSGET_SESSION_CHANGED) {
        if (this.connection._drcpEstablishSession) {
          this.connection.statementCache.clearCursors();
        }
      }
      this.connection._drcpEstablishSession = false;
      buf.skipUB4();                            // session id
      buf.skipUB2();                            // serial number
    } else {
      errors.throwErr(errors.ERR_UNKOWN_SERVER_SIDE_PIGGYBACK, opcode);
    }
  }

  writePiggybacks(buf) {
    if (this.connection._currentSchemaModified) {
      this._writeCurrentSchemaPiggyback(buf);
    }
    if (this.connection.statementCache._cursorsToClose.size > 0 && !this.connection._drcpEstablishSession) {
      // skip closing cursors when '_drcpEstablishSession = true' Since we don't know whether the same session
      // is in use. We can not send the information across until after the session information has been returned
      // (on the first round trip).
      this.writeCloseCursorsPiggyBack(buf);
    }
    if (
      this.connection._actionModified ||
      this.connection._clientIdentifierModified ||
      this.connection._dbopModified ||
      this.connection._clientInfoModified ||
      this.connection._moduleModified
    ) {
      this._writeEndToEndPiggybacks(buf);
    }
    if (this.connection._tempLobsTotalSize > 0) {
      this.writeCloseTempLobsPiggyback(buf);
    }
  }

  writePiggybackHeader(buf, functionCode) {
    buf.writeUInt8(constants.TNS_MSG_TYPE_PIGGYBACK);
    buf.writeUInt8(functionCode);
    buf.writeSeqNum();
    if (buf.caps.ttcFieldVersion >= constants.TNS_CCAP_FIELD_VERSION_23_1_EXT_1) {
      buf.writeUB8(0);                          // token number
    }
  }

  writeCloseCursorsPiggyBack(buf) {
    this.writePiggybackHeader(buf, constants.TNS_FUNC_CLOSE_CURSORS);
    buf.writeUInt8(1);
    this.connection.statementCache.writeCursorsToClose(buf);
  }

  writeCloseTempLobsPiggyback(buf) {
    const lobsToClose = this.connection._tempLobsToClose;
    const opCode = constants.TNS_LOB_OP_FREE_TEMP | constants.TNS_LOB_OP_ARRAY;

    this.writePiggybackHeader(buf, constants.TNS_FUNC_LOB_OP);

    buf.writeUInt8(1); // pointer
    buf.writeUB4(this.connection._tempLobsTotalSize);
    buf.writeUInt8(0); // dest LOB locator
    buf.writeUB4(0);
    buf.writeUB4(0); // source LOB locator
    buf.writeUB4(0);
    buf.writeUInt8(0); // source LOB offset
    buf.writeUInt8(0); // dest LOB offset
    buf.writeUInt8(0); // charset
    buf.writeUB4(opCode);
    buf.writeUInt8(0); // scn
    buf.writeUB4(0); // LOB scn
    buf.writeUB8(0); // LOB scnl
    buf.writeUB8(0);
    buf.writeUInt8(0);

    // array LOB fields
    buf.writeUInt8(0);
    buf.writeUB4(0);
    buf.writeUInt8(0);
    buf.writeUB4(0);
    buf.writeUInt8(0);
    buf.writeUB4(0);
    for (const val of lobsToClose) {
      buf.writeBytes(val);
    }

    // Reset Values
    this.connection._tempLobsToClose = [];
    this.connection._tempLobsTotalSize = 0;
  }

  _writeCurrentSchemaPiggyback(buf) {
    this.writePiggybackHeader(buf, constants.TNS_FUNC_SET_SCHEMA);
    buf.writeUInt8(1);
    const bytes = Buffer.byteLength(this.connection.currentSchema);
    buf.writeUB4(bytes);
    buf.writeBytesWithLength(Buffer.from(this.connection.currentSchema));
  }

  _writeEndToEndPiggybacks(buf) {
    let flags = 0;

    // determine which flags to send
    if (this.connection._actionModified) {
      flags |= constants.TNS_END_TO_END_ACTION;
    }
    if (this.connection._clientIdentifierModified) {
      flags |= constants.TNS_END_TO_END_CLIENT_IDENTIFIER;
    }
    if (this.connection._clientInfoModified) {
      flags |= constants.TNS_END_TO_END_CLIENT_INFO;
    }
    if (this.connection._moduleModified) {
      flags |= constants.TNS_END_TO_END_MODULE;
    }
    if (this.connection._dbOpModified) {
      flags |= constants.TNS_END_TO_END_DBOP;
    }

    // write initial packet data
    this.writePiggybackHeader(buf, constants.TNS_FUNC_SET_END_TO_END_ATTR);
    buf.writeUInt8(0);                  // pointer (cidnam)
    buf.writeUInt8(0);                  // pointer (cidser)
    buf.writeUB4(flags);

    const clientIdentifierBytes = this.writeEndEndTraceValue(buf, this.connection._clientIdentifier, this.connection._clientIdentifierModified);
    const moduleBytes = this.writeEndEndTraceValue(buf, this.connection._module, this.connection._moduleModified);
    const actionBytes = this.writeEndEndTraceValue(buf, this.connection._action, this.connection._actionModified);

    // write unsupported bits
    buf.writeUInt8(0);                  // pointer (cideci)
    buf.writeUB4(0);                    // length (cideci)
    buf.writeUInt8(0);                  // cidcct
    buf.writeUB4(0);                    // cidecs

    const clientInfoBytes = this.writeEndEndTraceValue(buf, this.connection._clientInfo, this.connection._clientInfoModified);
    // write unsupported bits
    buf.writeUInt8(0);                  // pointer (cideci)
    buf.writeUB4(0);                    // length (cideci)
    buf.writeUInt8(0);                  // cidcct
    buf.writeUB4(0);                    // cidecs
    const dbOpBytes = this.writeEndEndTraceValue(buf, this.connection._dbOp, this.connection._dbOpModified);

    // write strings
    if (this.connection._clientIdentifierModified && this.connection._clientIdentifier) {
      buf.writeBytesWithLength(clientIdentifierBytes);
    }
    if (this.connection._moduleModified && this.connection._module) {
      buf.writeBytesWithLength(moduleBytes);
    }
    if (this.connection._actionModified && this.connection._action) {
      buf.writeBytesWithLength(actionBytes);
    }
    if (this.connection._clientInfoModified && this.connection._clientInfo) {
      buf.writeBytesWithLength(clientInfoBytes);
    }
    if (this.connection._dbOpModified && this.connection._dbOp) {
      buf.writeBytesWithLength(dbOpBytes);
    }

    // reset flags and values
    this.connection._actionModified = false;
    this.connection._action = "";
    this.connection._clientIdentifierModified = false;
    this.connection._clientIdentifier = "";
    this.connection._clientInfoModified = false;
    this.connection._clientInfo = "";
    this.connection._dbOpModified = false;
    this.connection._dbOp = "";
    this.connection._moduleModified = false;
    this.connection._module = "";
  }

  writeEndEndTraceValue(buf, value, modified) {
    // write client identifier header info
    let writtenBytes;
    if (modified) {
      buf.writeUInt8(1);              // pointer (client identifier)
      if (value) {
        writtenBytes = Buffer.from(value);
        buf.writeUB4(writtenBytes.length);
      } else {
        buf.writeUB4(0);
      }
    } else {
      buf.writeUInt8(0);              // pointer (client identifier)
      buf.writeUB4(0);                // length of client identifier
    }
    return writtenBytes;
  }

  // Called when an error is encountered during decode of RPC
  saveDeferredErr() {
    if (!this.deferredErr) {
      this.deferredErr = errors.getErr(...arguments);
    }
  }
}

module.exports = Message;
