/*
 * Copyright (C) 2006 The Android Open Source Project
 *
 * 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.
 */

package com.android.internal.telephony.cat;

import android.compat.annotation.UnsupportedAppUsage;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.os.AsyncResult;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;

import com.android.internal.telephony.uicc.IccFileHandler;

import java.util.HashMap;

/**
 * Class for loading icons from the SIM card. Has two states: single, for loading
 * one icon. Multi, for loading icons list.
 *
 */
class IconLoader extends Handler {
    // members
    private int mState = STATE_SINGLE_ICON;
    private ImageDescriptor mId = null;
    private Bitmap mCurrentIcon = null;
    private int mRecordNumber;
    private IccFileHandler mSimFH = null;
    private Message mEndMsg = null;
    private byte[] mIconData = null;
    // multi icons state members
    private int[] mRecordNumbers = null;
    private int mCurrentRecordIndex = 0;
    private Bitmap[] mIcons = null;
    private HashMap<Integer, Bitmap> mIconsCache = null;

    private static IconLoader sLoader = null;
    private static HandlerThread sThread = null;

    // Loader state values.
    private static final int STATE_SINGLE_ICON = 1;
    private static final int STATE_MULTI_ICONS = 2;

    // Finished loading single record from a linear-fixed EF-IMG.
    private static final int EVENT_READ_EF_IMG_RECOED_DONE  = 1;
    // Finished loading single icon from a Transparent DF-Graphics.
    private static final int EVENT_READ_ICON_DONE           = 2;
    // Finished loading single colour icon lookup table.
    private static final int EVENT_READ_CLUT_DONE           = 3;

    // Color lookup table offset inside the EF.
    private static final int CLUT_LOCATION_OFFSET = 4;
    // CLUT entry size, {Red, Green, Black}
    private static final int CLUT_ENTRY_SIZE = 3;


    private IconLoader(Looper looper , IccFileHandler fh) {
        super(looper);
        mSimFH = fh;

        mIconsCache = new HashMap<Integer, Bitmap>(50);
    }

    static IconLoader getInstance(Handler caller, IccFileHandler fh) {
        if (sLoader != null) {
            return sLoader;
        }
        if (fh != null) {
            sThread = new HandlerThread("Cat Icon Loader");
            sThread.start();
            return new IconLoader(sThread.getLooper(), fh);
        }
        return null;
    }

    void loadIcons(int[] recordNumbers, Message msg) {
        if (recordNumbers == null || recordNumbers.length == 0 || msg == null) {
            return;
        }
        mEndMsg = msg;
        // initialize multi icons load variables.
        mIcons = new Bitmap[recordNumbers.length];
        mRecordNumbers = recordNumbers;
        mCurrentRecordIndex = 0;
        mState = STATE_MULTI_ICONS;
        startLoadingIcon(recordNumbers[0]);
    }

    @UnsupportedAppUsage
    void loadIcon(int recordNumber, Message msg) {
        if (msg == null) {
            return;
        }
        mEndMsg = msg;
        mState = STATE_SINGLE_ICON;
        startLoadingIcon(recordNumber);
    }

    private void startLoadingIcon(int recordNumber) {
        // Reset the load variables.
        mId = null;
        mIconData = null;
        mCurrentIcon = null;
        mRecordNumber = recordNumber;

        // make sure the icon was not already loaded and saved in the local cache.
        if (mIconsCache.containsKey(recordNumber)) {
            mCurrentIcon = mIconsCache.get(recordNumber);
            postIcon();
            return;
        }

        // start the first phase ==> loading Image Descriptor.
        readId();
    }

    @Override
    public void handleMessage(Message msg) {
        AsyncResult ar;

        try {
            switch (msg.what) {
            case EVENT_READ_EF_IMG_RECOED_DONE:
                ar = (AsyncResult) msg.obj;
                if (handleImageDescriptor((byte[]) ar.result)) {
                    readIconData();
                } else {
                    throw new Exception("Unable to parse image descriptor");
                }
                break;
            case EVENT_READ_ICON_DONE:
                CatLog.d(this, "load icon done");
                ar = (AsyncResult) msg.obj;
                byte[] rawData = ((byte[]) ar.result);
                if (mId.mCodingScheme == ImageDescriptor.CODING_SCHEME_BASIC) {
                    mCurrentIcon = parseToBnW(rawData, rawData.length);
                    mIconsCache.put(mRecordNumber, mCurrentIcon);
                    postIcon();
                } else if (mId.mCodingScheme == ImageDescriptor.CODING_SCHEME_COLOUR) {
                    mIconData = rawData;
                    readClut();
                } else {
                    CatLog.d(this, "else  /postIcon ");
                    postIcon();
                }
                break;
            case EVENT_READ_CLUT_DONE:
                ar = (AsyncResult) msg.obj;
                byte [] clut = ((byte[]) ar.result);
                mCurrentIcon = parseToRGB(mIconData, mIconData.length,
                        false, clut);
                mIconsCache.put(mRecordNumber, mCurrentIcon);
                postIcon();
                break;
            }
        } catch (Exception e) {
            CatLog.d(this, "Icon load failed");
            // post null icon back to the caller.
            postIcon();
        }
    }

    /**
     * Handles Image descriptor parsing and required processing. This is the
     * first step required to handle retrieving icons from the SIM.
     *
     * @param rawData byte [] containing Image Instance descriptor as defined in
     * TS 51.011.
     */
    private boolean handleImageDescriptor(byte[] rawData) {
        mId = ImageDescriptor.parse(rawData, 1);
        if (mId == null) {
            return false;
        }
        return true;
    }

    // Start reading color lookup table from SIM card.
    private void readClut() {
        int length = mIconData[3] * CLUT_ENTRY_SIZE;
        Message msg = obtainMessage(EVENT_READ_CLUT_DONE);
        mSimFH.loadEFImgTransparent(mId.mImageId,
                mIconData[CLUT_LOCATION_OFFSET],
                mIconData[CLUT_LOCATION_OFFSET + 1], length, msg);
    }

    // Start reading Image Descriptor from SIM card.
    private void readId() {
        if (mRecordNumber < 0) {
            mCurrentIcon = null;
            postIcon();
            return;
        }
        Message msg = obtainMessage(EVENT_READ_EF_IMG_RECOED_DONE);
        mSimFH.loadEFImgLinearFixed(mRecordNumber, msg);
    }

    // Start reading icon bytes array from SIM card.
    private void readIconData() {
        Message msg = obtainMessage(EVENT_READ_ICON_DONE);
        mSimFH.loadEFImgTransparent(mId.mImageId, 0, 0, mId.mLength ,msg);
    }

    // When all is done pass icon back to caller.
    private void postIcon() {
        if (mState == STATE_SINGLE_ICON) {
            mEndMsg.obj = mCurrentIcon;
            mEndMsg.sendToTarget();
        } else if (mState == STATE_MULTI_ICONS) {
            mIcons[mCurrentRecordIndex++] = mCurrentIcon;
            // If not all icons were loaded, start loading the next one.
            if (mCurrentRecordIndex < mRecordNumbers.length) {
                startLoadingIcon(mRecordNumbers[mCurrentRecordIndex]);
            } else {
                mEndMsg.obj = mIcons;
                mEndMsg.sendToTarget();
            }
        }
    }

    /**
     * Convert a TS 131.102 image instance of code scheme '11' into Bitmap
     * @param data The raw data
     * @param length The length of image body
     * @return The bitmap
     */
    public static Bitmap parseToBnW(byte[] data, int length){
        int valueIndex = 0;
        int width = data[valueIndex++] & 0xFF;
        int height = data[valueIndex++] & 0xFF;
        int numOfPixels = width*height;

        int[] pixels = new int[numOfPixels];

        int pixelIndex = 0;
        int bitIndex = 7;
        byte currentByte = 0x00;
        while (pixelIndex < numOfPixels) {
            // reassign data and index for every byte (8 bits).
            if (pixelIndex % 8 == 0) {
                currentByte = data[valueIndex++];
                bitIndex = 7;
            }
            pixels[pixelIndex++] = bitToBnW((currentByte >> bitIndex-- ) & 0x01);
        }

        if (pixelIndex != numOfPixels) {
            CatLog.d("IconLoader", "parseToBnW; size error");
        }
        return Bitmap.createBitmap(pixels, width, height, Bitmap.Config.ARGB_8888);
    }

    /**
     * Decode one bit to a black and white color:
     * 0 is black
     * 1 is white
     * @param bit to decode
     * @return RGB color
     */
    private static int bitToBnW(int bit){
        if(bit == 1){
            return Color.WHITE;
        } else {
            return Color.BLACK;
        }
    }

    /**
     * a TS 131.102 image instance of code scheme '11' into color Bitmap
     *
     * @param data The raw data
     * @param length the length of image body
     * @param transparency with or without transparency
     * @param clut coulor lookup table
     * @return The color bitmap
     */
    public static Bitmap parseToRGB(byte[] data, int length,
            boolean transparency, byte[] clut) {
        int valueIndex = 0;
        int width = data[valueIndex++] & 0xFF;
        int height = data[valueIndex++] & 0xFF;
        int bitsPerImg = data[valueIndex++] & 0xFF;
        int numOfClutEntries = data[valueIndex++] & 0xFF;

        if (true == transparency) {
            clut[numOfClutEntries - 1] = Color.TRANSPARENT;
        }

        int numOfPixels = width * height;
        int[] pixels = new int[numOfPixels];

        valueIndex = 6;
        int pixelIndex = 0;
        int bitsStartOffset = 8 - bitsPerImg;
        int bitIndex = bitsStartOffset;
        byte currentByte = data[valueIndex++];
        int mask = getMask(bitsPerImg);
        boolean bitsOverlaps = (8 % bitsPerImg == 0);
        while (pixelIndex < numOfPixels) {
            // reassign data and index for every byte (8 bits).
            if (bitIndex < 0) {
                currentByte = data[valueIndex++];
                bitIndex = bitsOverlaps ? (bitsStartOffset) : (bitIndex * -1);
            }
            int clutEntry = ((currentByte >> bitIndex) & mask);
            int clutIndex = clutEntry * CLUT_ENTRY_SIZE;
            pixels[pixelIndex++] = Color.rgb(clut[clutIndex],
                    clut[clutIndex + 1], clut[clutIndex + 2]);
            bitIndex -= bitsPerImg;
        }

        return Bitmap.createBitmap(pixels, width, height,
                Bitmap.Config.ARGB_8888);
    }

    /**
     * Calculate bit mask for a given number of bits. The mask should enable to
     * make a bitwise and to the given number of bits.
     * @param numOfBits number of bits to calculate mask for.
     * @return bit mask
     */
    private static int getMask(int numOfBits) {
        int mask = 0x00;

        switch (numOfBits) {
        case 1:
            mask = 0x01;
            break;
        case 2:
            mask = 0x03;
            break;
        case 3:
            mask = 0x07;
            break;
        case 4:
            mask = 0x0F;
            break;
        case 5:
            mask = 0x1F;
            break;
        case 6:
            mask = 0x3F;
            break;
        case 7:
            mask = 0x7F;
            break;
        case 8:
            mask = 0xFF;
            break;
        }
        return mask;
    }
    public void dispose() {
        mSimFH = null;
        if (sThread != null) {
            sThread.quit();
            sThread = null;
        }
        mIconsCache = null;
        sLoader = null;
    }
}
