/*
 *  Copyright (C) 2010-2013 JPEXS
 * 
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 * 
 *  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 for more details.
 * 
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.jpexs.decompiler.flash.abc;

import com.jpexs.decompiler.flash.abc.types.*;
import com.jpexs.decompiler.flash.abc.types.traits.*;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

public class ABCInputStream extends InputStream {

    private static final int CLASS_PROTECTED_NS = 8;
    private static final int ATTR_METADATA = 4;
    private InputStream is;
    private long bytesRead = 0;
    private ByteArrayOutputStream bufferOs = null;
    public static final boolean DEBUG_READ = false;

    public void startBuffer() {
        bufferOs = new ByteArrayOutputStream();
    }

    public byte[] stopBuffer() {
        if (bufferOs == null) {
            return new byte[0];
        }
        byte ret[] = bufferOs.toByteArray();
        bufferOs = null;
        return ret;
    }

    public ABCInputStream(InputStream is) {
        this.is = is;
    }

    @Override
    public int read() throws IOException {
        bytesRead++;
        int i = is.read();
        if (DEBUG_READ) {
            System.out.println("Read:0x" + Integer.toHexString(i));
        }
        if (bufferOs != null) {
            if (i != -1) {
                bufferOs.write(i);
            }
        }
        return i;
    }

    @Override
    public int read(byte[] b) throws IOException {
        int currBytesRead = is.read(b);
        bytesRead += currBytesRead;
        if (DEBUG_READ) {
            StringBuilder sb = new StringBuilder("Read[");
            sb.append(currBytesRead);
            sb.append('/');
            sb.append(b.length);
            sb.append("]: ");
            for (int jj = 0; jj < currBytesRead; jj++) {
                sb.append("0x");
                sb.append(Integer.toHexString(b[jj]));
                sb.append(' ');
            }
            System.out.println(sb.toString());
        }
        if (bufferOs != null) {
            if (currBytesRead > 0) {
                bufferOs.write(b, 0, currBytesRead);
            }
        }
        return currBytesRead;
    }

    ;

    public int readU8() throws IOException {
        return read();
    }

    public int readU32() throws IOException {
        int i;
        int ret = 0;
        int bytePos = 0;
        int byteCount = 0;
        boolean nextByte;
        do {
            i = read();
            nextByte = (i >> 7) == 1;
            i = i & 0x7f;
            ret = ret + (i << bytePos);
            byteCount++;
            bytePos += 7;
        } while (nextByte);
        return ret;
    }

    public int readU30() throws IOException {
        return readU32();
    }

    public int readS24() throws IOException {
        int ret = (read()) + (read() << 8) + (read() << 16);

        if ((ret >> 23) == 1) {
            ret = ret | 0xff000000;
        }

        return ret;
    }

    public int readU16() throws IOException {
        return (read()) + (read() << 8);
    }

    public long readS32() throws IOException {
        int i;
        long ret = 0;
        int bytePos = 0;
        int byteCount = 0;
        boolean nextByte;
        do {
            i = read();
            nextByte = (i >> 7) == 1;
            i = i & 0x7f;
            ret = ret + (i << bytePos);
            byteCount++;
            bytePos += 7;
            if (bytePos == 35) {
                if ((ret >> 31) == 1) {
                    ret = -(ret & 0x7fffffff);
                }
                break;
            }
        } while (nextByte);
        return ret;
    }

    @Override
    public int available() throws IOException {
        return is.available();
    }

    public final long readLong() throws IOException {
        byte readBuffer[] = safeRead(8);
        return (((long) readBuffer[7] << 56)
                + ((long) (readBuffer[6] & 255) << 48)
                + ((long) (readBuffer[5] & 255) << 40)
                + ((long) (readBuffer[4] & 255) << 32)
                + ((long) (readBuffer[3] & 255) << 24)
                + ((readBuffer[2] & 255) << 16)
                + ((readBuffer[1] & 255) << 8)
                + ((readBuffer[0] & 255)));
    }

    public double readDouble() throws IOException {
        long el = readLong();
        double ret = Double.longBitsToDouble(el);
        return ret;
    }

    private byte[] safeRead(int count) throws IOException {
        byte ret[] = new byte[count];
        for (int i = 0; i < count; i++) {
            ret[i] = (byte) read();
        }
        return ret;
    }

    public Namespace readNamespace() throws IOException {
        int kind = read();
        int name_index = 0;
        for (int k = 0; k < Namespace.nameSpaceKinds.length; k++) {
            if (Namespace.nameSpaceKinds[k] == kind) {
                name_index = readU30();
                break;
            }
        }
        return new Namespace(kind, name_index);
    }

    public Multiname readMultiname() throws IOException {
        int kind = readU8();
        int namespace_index = -1;
        int name_index = -1;
        int namespace_set_index = -1;
        int qname_index = -1;
        List<Integer> params = new ArrayList<>();

        if ((kind == 7) || (kind == 0xd)) { // CONSTANT_QName and CONSTANT_QNameA.
            namespace_index = readU30();
            name_index = readU30();
        } else if ((kind == 0xf) || (kind == 0x10)) { //CONSTANT_RTQName and CONSTANT_RTQNameA
            name_index = readU30();
        } else if ((kind == 0x11) || (kind == 0x12))//kind==0x11,0x12 nothing CONSTANT_RTQNameL and CONSTANT_RTQNameLA.
        {
        } else if ((kind == 9) || (kind == 0xe)) { // CONSTANT_Multiname and CONSTANT_MultinameA.
            name_index = readU30();
            namespace_set_index = readU30();
        } else if ((kind == 0x1B) || (kind == 0x1C)) { //CONSTANT_MultinameL and CONSTANT_MultinameLA
            namespace_set_index = readU30();
        } else if (kind == 0x1D) {
            //Constant_TypeName
            qname_index = readU30(); //Multiname index!!!
            int paramsLength = readU30();
            for (int i = 0; i < paramsLength; i++) {
                params.add(readU30()); //multiname indices!
            }
        } else {
            throw new IOException("Unknown kind of Multiname:0x" + Integer.toHexString(kind));
        }

        return new Multiname(kind, name_index, namespace_index, namespace_set_index, qname_index, params);
    }

    public MethodInfo readMethodInfo() throws IOException {
        int param_count = readU30();
        int ret_type = readU30();
        int param_types[] = new int[param_count];
        for (int i = 0; i < param_count; i++) {
            param_types[i] = readU30();
        }
        int name_index = readU30();
        int flags = read();

        //// 1=need_arguments, 2=need_activation, 4=need_rest 8=has_optional (16=ignore_rest, 32=explicit,) 64=setsdxns, 128=has_paramnames

        ValueKind optional[] = new ValueKind[0];
        if ((flags & 8) == 8) { //if has_optional
            int optional_count = readU30();
            optional = new ValueKind[optional_count];
            for (int i = 0; i < optional_count; i++) {
                optional[i] = new ValueKind(readU30(), read());
            }
        }

        int param_names[] = new int[param_count];
        if ((flags & 128) == 128) { //if has_paramnames
            for (int i = 0; i < param_count; i++) {
                param_names[i] = readU30();
            }
        }
        return new MethodInfo(param_types, ret_type, name_index, flags, optional, param_names);
    }

    public Trait readTrait() throws IOException {
        long pos = getPosition();
        startBuffer();
        int name_index = readU30();
        int kind = read();
        int kindType = 0xf & kind;
        int kindFlags = kind >> 4;
        Trait trait;

        switch (kindType) {
            case 0: //slot
            case 6: //const
                TraitSlotConst t1 = new TraitSlotConst();
                t1.slot_id = readU30();
                t1.type_index = readU30();
                t1.value_index = readU30();
                if (t1.value_index != 0) {
                    t1.value_kind = read();
                }
                trait = t1;
                break;
            case 1: //method
            case 2: //getter
            case 3: //setter
                TraitMethodGetterSetter t2 = new TraitMethodGetterSetter();
                t2.disp_id = readU30();
                t2.method_info = readU30();
                trait = t2;
                break;
            case 4: //class
                TraitClass t3 = new TraitClass();
                t3.slot_id = readU30();
                t3.class_info = readU30();
                trait = t3;
                break;
            case 5: //function
                TraitFunction t4 = new TraitFunction();
                t4.slot_index = readU30();
                t4.method_info = readU30();
                trait = t4;
                break;
            default:
                throw new IOException("Unknown trait kind:" + kind);
        }
        trait.fileOffset = pos;
        trait.kindType = kindType;
        trait.kindFlags = kindFlags;
        trait.name_index = name_index;
        if ((kindFlags & ATTR_METADATA) != 0) {
            int metadata_count = readU30();
            trait.metadata = new int[metadata_count];
            for (int i = 0; i < metadata_count; i++) {
                trait.metadata[i] = readU30();
            }
        }
        trait.bytes = stopBuffer();
        return trait;
    }

    public Traits readTraits() throws IOException {
        int count = readU30();
        Traits traits = new Traits();
        traits.traits = new Trait[count];
        for (int i = 0; i < count; i++) {
            traits.traits[i] = readTrait();
        }
        return traits;
    }

    public byte[] readBytes(int count) throws IOException {
        byte ret[] = new byte[count];
        for (int i = 0; i < count; i++) {
            ret[i] = (byte) read();
        }
        return ret;
    }

    public Decimal readDecimal() throws IOException {
        byte data[] = readBytes(16);
        return new Decimal(data);
    }

    public InstanceInfo readInstanceInfo() throws IOException {
        InstanceInfo ret = new InstanceInfo();
        ret.name_index = readU30();
        ret.super_index = readU30();
        ret.flags = read();
        if ((ret.flags & CLASS_PROTECTED_NS) != 0) {
            ret.protectedNS = readU30();
        }
        int interfaces_count = readU30();
        ret.interfaces = new int[interfaces_count];
        for (int i = 0; i < interfaces_count; i++) {
            ret.interfaces[i] = readU30();
        }
        ret.iinit_index = readU30();
        ret.instance_traits = readTraits();
        return ret;
    }

    public String readString() throws IOException {
        int length = readU30();
        byte b[] = safeRead(length);
        String r = new String(b, "UTF-8");
        return r;
    }


    /*public void markStart(){
     bytesRead=0;
     }*/
    public long getPosition() {
        return bytesRead;
    }
}
