/*
   Copyright (c) 2010, 2021, Oracle and/or its affiliates.

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License, version 2.0,
   as published by the Free Software Foundation.

   This program is also distributed with certain software (including
   but not limited to OpenSSL) that is licensed under separate terms,
   as designated in a particular file or component or in included license
   documentation.  The authors of MySQL hereby grant you an additional
   permission to link the program and your derivative works with the
   separately licensed software that they have included with MySQL.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License, version 2.0, for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
 */

package com.mysql.clusterj.tie;

import java.math.BigDecimal;
import java.math.BigInteger;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;

import java.util.List;

import com.mysql.clusterj.ClusterJFatalInternalException;

import com.mysql.clusterj.core.store.Blob;
import com.mysql.clusterj.core.store.Column;
import com.mysql.clusterj.core.store.ResultData;

import com.mysql.clusterj.core.util.I18NHelper;
import com.mysql.clusterj.core.util.Logger;
import com.mysql.clusterj.core.util.LoggerFactoryService;
import com.mysql.clusterj.tie.DbImpl.BufferManager;

import com.mysql.ndbjtie.ndbapi.NdbBlob;
import com.mysql.ndbjtie.ndbapi.NdbErrorConst;
import com.mysql.ndbjtie.ndbapi.NdbOperation;
import com.mysql.ndbjtie.ndbapi.NdbRecAttr;

/**
 *
 */
class ResultDataImpl implements ResultData {

    /** My message translator */
    static final I18NHelper local = I18NHelper
            .getInstance(ResultDataImpl.class);

    /** My logger */
    static final Logger logger = LoggerFactoryService.getFactory()
            .getInstance(ResultDataImpl.class);

    /** Flags for iterating a scan */
    protected final int RESULT_READY = 0;
    protected final int SCAN_FINISHED = 1;
    protected final int CACHE_EMPTY = 2;

    /** The NdbOperation that defines the result */
    private NdbOperation ndbOperation = null;

    /** The NdbRecAttrs that specify the columns to retrieve */
    private NdbRecAttr[] ndbRecAttrs = null;

    /** The flag indicating that there are no more results */
    private boolean nextDone;

    /** The ByteBuffer containing the results, possibly obtained from buffer manager */
    private ByteBuffer byteBuffer = null;

    /** Offsets into the ByteBuffer containing the results */
    private int[] offsets = null;

    /** Lengths of the fields in the ByteBuffer containing the results */
    private int[] lengths = null;

    /** The Columns in this result */
    private final Column[] storeColumns;

    /** The buffer manager */
    private BufferManager bufferManager;

    /** The cluster connection */
    private ClusterConnectionImpl clusterConnection;

    /** Construct the ResultDataImpl based on an NdbOperation, a list of columns
     * to include in the result, and the pre-computed buffer layout for the result.
     * @param ndbOperation the NdbOperation
     * @param storeColumns the columns in the result
     * @param maximumColumnId the largest column id
     * @param bufferSize the size of the buffer needed
     * @param offsets the array of offsets indexed by column id
     * @param lengths the array of lengths indexed by column id
     * @param bufferManager the buffer manager
     * @param allocateNew true to allocate a new (unshared) result buffer
     */
    public ResultDataImpl(NdbOperation ndbOperation, List<Column> storeColumns,
            int maximumColumnId, int bufferSize, int[] offsets, int[] lengths,
            BufferManager bufferManager, boolean allocateNew) {
        this.ndbOperation = ndbOperation;
        this.bufferManager = bufferManager;
        // save the column list
        this.storeColumns = storeColumns.toArray(new Column[storeColumns.size()]);
        this.offsets = offsets;
        this.lengths = lengths;
        if (allocateNew) {
            byteBuffer = ByteBuffer.allocateDirect(bufferSize);
        } else {
            byteBuffer = bufferManager.getResultDataBuffer(bufferSize);
        }
        byteBuffer.order(ByteOrder.nativeOrder());
        // iterate the list of store columns and allocate an NdbRecAttr (via getValue) for each
        ndbRecAttrs = new NdbRecAttr[maximumColumnId + 1];
        for (Column storeColumn: storeColumns) {
            NdbRecAttr ndbRecAttr = null;
            int columnId = storeColumn.getColumnId();
            byteBuffer.position(offsets[columnId]);
            if (lengths[columnId] == 0) {
                // TODO: to help profiling
                ndbRecAttr = ndbOperation.getValue(columnId, null);
//                ndbRecAttr = getValue(ndbOperation, columnId, null);
            } else {
                ndbRecAttr = ndbOperation.getValue(columnId, byteBuffer);
//                ndbRecAttr = getValue(ndbOperation, columnId, byteBuffer);
            }
            handleError(ndbRecAttr, ndbOperation);
            ndbRecAttrs[columnId] = ndbRecAttr;
        }
    }

    public boolean next() {
        // NdbOperation has exactly zero or one result. ScanResultDataImpl handles scans...
        NdbErrorConst error = ndbOperation.getNdbError();
        // if the ndbOperation reports an error there is no result
        int errorCode = error.code();
        if (errorCode != 0) {
            setNoResult();
        }
        if (nextDone) {
            return false;
        } else {
            nextDone = true;
            return true;
        }
    }

    public Blob getBlob(int column) {
        return getBlob(storeColumns[column]);
    }

    public Blob getBlob(Column storeColumn) {
        NdbBlob ndbBlob = ndbOperation.getBlobHandle(storeColumn.getColumnId());
        handleError(ndbBlob, ndbOperation);
        return new BlobImpl(ndbBlob, clusterConnection.getByteBufferPool());
    }

    public boolean getBoolean(int column) {
        return getBoolean(storeColumns[column]);
    }

    public boolean getBoolean(Column storeColumn) {
        int index = storeColumn.getColumnId();
        NdbRecAttr ndbRecAttr = ndbRecAttrs[index];
        return Utility.getBoolean(storeColumn, ndbRecAttr);
    }

    public boolean[] getBooleans(int column) {
        return getBooleans(storeColumns[column]);
    }

    public boolean[] getBooleans(Column storeColumn) {
        throw new ClusterJFatalInternalException(local.message("ERR_Not_Implemented"));
    }

    public byte getByte(int column) {
        return getByte(storeColumns[column]);
    }

    public byte getByte(Column storeColumn) {
        int index = storeColumn.getColumnId();
        NdbRecAttr ndbRecAttr = ndbRecAttrs[index];
        return Utility.getByte(storeColumn, ndbRecAttr);
    }

    public short getShort(int column) {
        return getShort(storeColumns[column]);
    }

    public short getShort(Column storeColumn) {
        int index = storeColumn.getColumnId();
        NdbRecAttr ndbRecAttr = ndbRecAttrs[index];
        return Utility.getShort(storeColumn, ndbRecAttr);
     }

    public int getInt(int column) {
        return getInt(storeColumns[column]);
    }

    public int getInt(Column storeColumn) {
        int index = storeColumn.getColumnId();
        NdbRecAttr ndbRecAttr = ndbRecAttrs[index];
        return Utility.getInt(storeColumn, ndbRecAttr);
    }

    public long getLong(int column) {
        return getLong(storeColumns[column]);
    }

    public long getLong(Column storeColumn) {
        int index = storeColumn.getColumnId();
        NdbRecAttr ndbRecAttr = ndbRecAttrs[index];
        return Utility.getLong(storeColumn, ndbRecAttr.int64_value());
     }

    public float getFloat(int column) {
        return getFloat(storeColumns[column]);
    }

    public float getFloat(Column storeColumn) {
        int index = storeColumn.getColumnId();
        float result = ndbRecAttrs[index].float_value();
        return result;
    }

    public double getDouble(int column) {
        return getDouble(storeColumns[column]);
    }

    public double getDouble(Column storeColumn) {
        int index = storeColumn.getColumnId();
        double result = ndbRecAttrs[index].double_value();
        return result;
    }

    public String getString(int column) {
        return getString(storeColumns[column]);
    }

    public String getString(Column storeColumn) {
        int index = storeColumn.getColumnId();
        NdbRecAttr ndbRecAttr = ndbRecAttrs[index];
        if (ndbRecAttr.isNULL() == 1) return null;
        int prefixLength = storeColumn.getPrefixLength();
        int actualLength;
        int offset = offsets[index];
        byteBuffer.limit(byteBuffer.capacity());
        switch (prefixLength) {
            case 0:
                actualLength = lengths[index];
                break;
            case 1:
                actualLength = (byteBuffer.get(offset) + 256) % 256;
                offset += 1;
                break;
            case 2:
                actualLength = (byteBuffer.get(offset) + 256) % 256;
                int length2 = (byteBuffer.get(offset + 1) + 256) % 256;
                actualLength += 256 * length2;
                offset += 2;
                break;
            default:
                throw new ClusterJFatalInternalException(
                        local.message("ERR_Invalid_Prefix_Length", prefixLength));
        }

        byteBuffer.position(offset);
        byteBuffer.limit(offset + actualLength);

        String result = Utility.decode(byteBuffer, storeColumn.getCharsetNumber(), bufferManager);
        byteBuffer.clear();
        return result;
    }

    public byte[] getBytes(int column) {
        return getBytes(storeColumns[column]);
    }

    public byte[] getBytes(Column storeColumn) {
        int index = storeColumn.getColumnId();
        NdbRecAttr ndbRecAttr = ndbRecAttrs[index];
        if (ndbRecAttr.isNULL() == 1) return null;
        int prefixLength = storeColumn.getPrefixLength();
        int actualLength = lengths[index];
        int offset = offsets[index];
        switch (prefixLength) {
            case 0:
                break;
            case 1:
                actualLength = (byteBuffer.get(offset) + 256) % 256;
                offset += 1;
                break;
            case 2:
                actualLength = (byteBuffer.get(offset) + 256) % 256;
                int length2 = (byteBuffer.get(offset + 1) + 256) % 256;
                actualLength += 256 * length2;
                offset += 2;
                break;
            default:
                throw new ClusterJFatalInternalException(
                        local.message("ERR_Invalid_Prefix_Length", prefixLength));
        }
        byteBuffer.position(offset);
        byte[] result = new byte[actualLength];
        byteBuffer.get(result);
        return result;
     }


    public Object getObject(int column) {
        return getObject(storeColumns[column]);
    }

    public Object getObject(Column storeColumn) {
        throw new ClusterJFatalInternalException(local.message("ERR_Implementation_Should_Not_Occur"));
    }

    public boolean wasNull(Column storeColumn) {
        throw new ClusterJFatalInternalException(local.message("ERR_Implementation_Should_Not_Occur"));
    }

    public Boolean getObjectBoolean(int column) {
        return getObjectBoolean(storeColumns[column]);
    }

    public Boolean getObjectBoolean(Column storeColumn) {
        int index = storeColumn.getColumnId();
        NdbRecAttr ndbRecAttr = ndbRecAttrs[index];
        if (ndbRecAttr.isNULL() == 1) {
            return null;
        } else {
            byte value = ndbRecAttr.int8_value();
            Boolean result = (Boolean.valueOf((value & 0x01) == 0x01));
            return result;
        }
    }

    public Byte getObjectByte(int column) {
        return getObjectByte(storeColumns[column]);
    }

    public Byte getObjectByte(Column storeColumn) {
        int index = storeColumn.getColumnId();
        NdbRecAttr ndbRecAttr = ndbRecAttrs[index];
        return (ndbRecAttr.isNULL() == 1)?null:Utility.getByte(storeColumn, ndbRecAttr);
    }

    public Short getObjectShort(int column) {
        return getObjectShort(storeColumns[column]);
    }

    public Short getObjectShort(Column storeColumn) {
        int index = storeColumn.getColumnId();
        NdbRecAttr ndbRecAttr = ndbRecAttrs[index];
        return (ndbRecAttr.isNULL() == 1)?null:Utility.getShort(storeColumn, ndbRecAttr);
    }

    public Integer getObjectInteger(int column) {
        return getObjectInteger(storeColumns[column]);
    }

    public Integer getObjectInteger(Column storeColumn) {
        int index = storeColumn.getColumnId();
        NdbRecAttr ndbRecAttr = ndbRecAttrs[index];
        return (ndbRecAttr.isNULL() == 1)?null:Utility.getInt(storeColumn, ndbRecAttr);
    }

    public Long getObjectLong(int column) {
        return getObjectLong(storeColumns[column]);
    }

    public Long getObjectLong(Column storeColumn) {
        int index = storeColumn.getColumnId();
        NdbRecAttr ndbRecAttr = ndbRecAttrs[index];
        return (ndbRecAttr.isNULL() == 1)?null:Utility.getLong(storeColumn, ndbRecAttr.int64_value());
    }

    public Float getObjectFloat(int column) {
        return getObjectFloat(storeColumns[column]);
    }

    public Float getObjectFloat(Column storeColumn) {
        int index = storeColumn.getColumnId();
        NdbRecAttr ndbRecAttr = ndbRecAttrs[index];
        return (ndbRecAttr.isNULL() == 1)?null:getFloat(storeColumn);
    }

    public Double getObjectDouble(int column) {
        return getObjectDouble(storeColumns[column]);
    }

    public Double getObjectDouble(Column storeColumn) {
        int index = storeColumn.getColumnId();
        NdbRecAttr ndbRecAttr = ndbRecAttrs[index];
        return (ndbRecAttr.isNULL() == 1)?null:getDouble(storeColumn);
    }

    public BigInteger getBigInteger(int column) {
        return getBigInteger(storeColumns[column]);
    }

    public BigInteger getBigInteger(Column storeColumn) {
        int index = storeColumn.getColumnId();
        int offset = offsets[index];
        int precision = storeColumn.getPrecision();
        int scale = storeColumn.getScale();
        int length = Utility.getDecimalColumnSpace(precision, scale);
        byteBuffer.position(offset);
        return Utility.getBigInteger(byteBuffer, length, precision, scale);
    }

    public BigDecimal getDecimal(int column) {
        return getDecimal(storeColumns[column]);
    }

    public BigDecimal getDecimal(Column storeColumn) {
        int index = storeColumn.getColumnId();
        int offset = offsets[index];
        int precision = storeColumn.getPrecision();
        int scale = storeColumn.getScale();
        int length = Utility.getDecimalColumnSpace(precision, scale);
        byteBuffer.position(offset);
        return Utility.getDecimal(byteBuffer, length, precision, scale);
    }

    private void handleError(Object object, NdbOperation ndbOperation) {
        if (object == null) {
            Utility.throwError(object, ndbOperation.getNdbError());
        }
    }

    public void setNoResult() {
        nextDone = true;
    }

    public Column[] getColumns() {
        return storeColumns;
    }

}
