/*
 * Copyright (C) 2014 Alexey Illarionov
 *
 * 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 zz.aimsicd.lite.rilexecutor;

import android.net.LocalSocket;
import android.net.LocalSocketAddress;
import android.os.Message;
import android.os.Parcel;
import android.util.Log;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.atomic.AtomicBoolean;


public class SamsungMulticlientRilExecutor implements OemRilExecutor {

    public static final String TAG = "AICDL";
    public static final String mTAG = "SEC_RIL";

    private static final String MULTICLIENT_SOCKET = "Multiclient";
    private static final int RIL_REQUEST_OEM_RAW = 59;
    private static final int RIL_REQUEST_OEM_STRINGS = 60;
    private static final int RIL_CLIENT_ERR_SUCCESS = 0;
    private static final int RESPONSE_SOLICITED = 0;
    private static final int RESPONSE_UNSOLICITED = 1;
    private static final int ID_REQUEST_AT_COMMAND = 5;
    private static final int ID_RESPONSE_AT_COMMAND = 104;

    // If you need debugging on, for testing on other devices, change here.
    //  WARNING: Could fill your logcat if running app long!
    // private static final boolean DBG = BuildConfig.DEBUG;
    private static final boolean DBG = false;

    private volatile LocalSocketThread mThread;

    @Override
    public DetectResult detect() {
        String gsmVerRilImpl = "";

        try {
            Class clazz;
            clazz = Class.forName("android.os.SystemProperties");
            Method method = clazz.getDeclaredMethod("get", String.class, String.class);
            gsmVerRilImpl = (String) method.invoke(null, "gsm.version.ril-impl", "");
        } catch (Exception ignore) {
           Log.d(TAG, mTAG + "ignore this exception?", ignore);
        }

        // E:V:A comment out for debugging purposes on other non-Samsung RILS
        // and moved gsm.version.. to catch...
        // WARNING may have bad consequences...
        if (!gsmVerRilImpl.matches("Samsung\\s+RIL\\(IPC\\).*")) {
            return DetectResult.Unavailable("gsm.version.ril-impl = " + gsmVerRilImpl);
        }

        LocalSocket s = new LocalSocket();
        try {
            s.connect(new LocalSocketAddress(MULTICLIENT_SOCKET));
        } catch (IOException e) {
            Log.w(TAG, mTAG + e.getMessage());
            return DetectResult.Unavailable("Multiclient socket is not available\n" + "gsm.version.ril-impl = " + gsmVerRilImpl);
        } finally {
            try {
                s.close();
            } catch (IOException e) {
                Log.e(TAG, mTAG + e.getMessage(), e);
            }
        }

        return DetectResult.AVAILABLE;
    }

    @Override
    public synchronized void start() {
        if (mThread != null) {
           Log.e(TAG, mTAG + "OEM raw request executor thread is running");
            return;
        }
        mThread = new LocalSocketThread(MULTICLIENT_SOCKET);
        mThread.start();
    }

    @Override
    public synchronized void stop() {
        if (mThread == null) {
           Log.e(TAG, mTAG + "OEM raw request executor thread is not running");
            return;
        }
        mThread.cancel();
        mThread = null;
    }

    @Override
    public synchronized void invokeOemRilRequestRaw(byte[] data, Message response) {
        if (mThread == null) {
           Log.e(TAG, mTAG + " OEM raw request executor thread is not running");
            return;
        }
        try {
            mThread.invokeOemRilRequestRaw(data, response);
        } catch (IOException ioe) {
           Log.e(TAG, mTAG + "InvokeOemRilRequestRaw() error", ioe);
        }
    }

    @Override
    public synchronized void invokeOemRilRequestStrings(String[] strings, Message response) {
        if (mThread == null) {
           Log.e(TAG, mTAG + "OEM raw request executor thread is not running");
            return;
        }
        try {
            mThread.invokeOemRilRequestStrings(strings, response);
        } catch (IOException ioe) {
           Log.e(TAG, mTAG + "InvokeOemRilRequestStrings() error", ioe);
        }
    }

    public class LocalSocketThread extends Thread {

        private static final int MAX_MESSAGES = 30;
        private final LocalSocketAddress mSocketPath;
        private final AtomicBoolean mCancelRequested = new AtomicBoolean();
        private LocalSocket mSocket;
        private volatile InputStream mInputStream;
        private volatile OutputStream mOutputStream;
        private final Random mTokenGen = new Random();
        private final Map<Integer, Message> mMessages;

        //@SuppressLint("UseSparseArrays")
        public LocalSocketThread(String socketPath) {
            mSocketPath = new LocalSocketAddress(socketPath);
            mInputStream = null;
            mOutputStream = null;
            mMessages = new HashMap<>();
        }

        public void cancel() {
            if (DBG) {
                Log.i(TAG, mTAG + "SamsungMulticlientRil cancel()");
            }
            synchronized (this) {
                mCancelRequested.set(true);
                disconnect();
                notifyAll();
            }
        }

        public synchronized void invokeOemRilRequestRaw(byte[] data, Message response)
                throws IOException {
            int token;
            if (mMessages.size() > MAX_MESSAGES) {
               Log.e(TAG, mTAG + "message queue is full");
                return;
            }

            if (mOutputStream == null) {
               Log.e(TAG, mTAG + "Local write() error: not connected");
                return;
            }

            do {
                token = mTokenGen.nextInt();
            } while (mMessages.containsKey(token));

            byte req[] = marshallRequest(token, data);

            if (DBG) {
                Log.i(TAG, mTAG + String.format("InvokeOemRilRequestRaw() token: 0x%X, header: %s, req: %s ",
                                token, HexDump.toHexString(getHeader(req)), HexDump.toHexString(req))
                );
            }

            mOutputStream.write(getHeader(req));
            mOutputStream.write(req);
            mMessages.put(token, response);
        }

        public synchronized void invokeOemRilRequestStrings(String strings[], Message response)
                throws IOException {
            int token;
            if (mMessages.size() > MAX_MESSAGES) {
               Log.e(TAG, mTAG + "Message queue is full");
                return;
            }

            if (mOutputStream == null) {
               Log.e(TAG, mTAG + "Local write() error: not connected");
                return;
            }

            do {
                token = mTokenGen.nextInt();
            } while (mMessages.containsKey(token));

            byte[] req = marshallRequest(token, strings);

            if (DBG) {
                Log.i(TAG, mTAG + String.format("InvokeOemRilRequestStrings() token: 0x%X, header: %s, req: %s ",
                        token, HexDump.toHexString(getHeader(req)), HexDump.toHexString(req)));
            }

            mOutputStream.write(getHeader(req));
            mOutputStream.write(req);
            mMessages.put(token, response);
        }

        private byte[] getHeader(byte data[]) {
            int len = data.length;
            return new byte[] {
                    (byte) ((len >> 24) & 0xff),
                    (byte) ((len >> 16) & 0xff),
                    (byte) ((len >> 8) & 0xff),
                    (byte) (len & 0xff)
            };
        }

        private byte[] marshallRequest(int token, byte data[]) {
            Parcel p = Parcel.obtain();
            p.writeInt(RIL_REQUEST_OEM_RAW);
            p.writeInt(token);
            p.writeByteArray(data);
            byte[] res = p.marshall();
            p.recycle();
            return res;
        }

        private byte[] marshallRequest(int token, String strings[]) {
            Parcel p = Parcel.obtain();
            p.writeInt(RIL_REQUEST_OEM_STRINGS);
            p.writeInt(token);
            p.writeStringArray(strings);
            byte[] res = p.marshall();
            p.recycle();
            return res;
        }

        public synchronized void disconnect() {

            if (DBG) {
                Log.i(TAG, mTAG + "Local disconnect()");
            }

            if (mSocket == null) {
                return;
            }

            try {
                mSocket.shutdownInput();
            } catch (IOException e) {
               Log.e(TAG, mTAG + "Local shutdownInput() of mSocket failed", e);
            }

            try {
                mSocket.shutdownOutput();
            } catch (IOException e) {
               Log.e(TAG, mTAG + "Local shutdownOutput() of mSocket failed", e);
            }

            try {
                mInputStream.close();
            } catch (IOException e) {
               Log.e(TAG, mTAG + "Local close() of mInputStream failed", e);
            }

            try {
                mOutputStream.close();
            } catch (IOException e) {
               Log.e(TAG, mTAG + "Local close() of mOutputStream failed", e);
            }

            try {
                mSocket.close();
            } catch (IOException e) {
               Log.e(TAG, mTAG + "Local close() of mSocket failed", e);
            }

            mSocket = null;
            mInputStream = null;
            mOutputStream = null;
            System.gc();
        }

        @Override
        public void run() {
            int rcvd;
            int endpos = 0;
            final byte buf[] = new byte[4096];

            Log.i(TAG, mTAG + "BEGIN LocalSocketThread-Socket");
            setName("MultiClientThread");

            mSocket = new LocalSocket();
            try {
                mSocket.connect(mSocketPath);
                mInputStream = mSocket.getInputStream();
                mOutputStream = mSocket.getOutputStream();
            } catch (IOException e) {
               Log.e(TAG, mTAG + "Connect error", e);
                return;
            }

            while (!mCancelRequested.get()) {
                try {
                    rcvd = mInputStream.read(buf, endpos, buf.length - endpos);
                    if (rcvd < 0) {
                        if (DBG) {
                            Log.i(TAG, mTAG + "EOF reached");
                        }
                        break;
                    }
                    endpos += rcvd;
                    if (endpos < 4) {
                        continue;
                    }

                    int msgLen = (buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | (buf[3] & 0xff);
                    if (msgLen + 4 > buf.length) {
                       Log.e(TAG, mTAG + "Message to big. Length: " + msgLen);
                        endpos = 0;
                        continue;
                    }
                    if (endpos >= msgLen + 4) {
                        processRxPacket(buf, 4, msgLen);
                        int secondPktPos = msgLen + 4;
                        if (secondPktPos != endpos) {
                            System.arraycopy(buf, secondPktPos, buf, 0, endpos - secondPktPos);
                        }
                        endpos -= msgLen + 4;
                    }

                    if (endpos == buf.length) {
                        endpos = 0;
                    }
                } catch (IOException e) {
                    disconnect();
                }
            }
            disconnect();
        }

        private synchronized void processRxPacket(byte data[], int pos, int length) {
            int responseType;
            Parcel p;

            if (DBG) {
                Log.i(TAG, mTAG + "Received " + length + " bytes: " +
                        HexDump.toHexString(data, pos, length));
            }

            p = Parcel.obtain();
            try {
                p.unmarshall(data, pos, length);
                p.setDataPosition(0);

                responseType = p.readInt();
                switch (responseType) {
                    case RESPONSE_UNSOLICITED:
                        Log.i(TAG, mTAG + "Unsolicited response ");
                        break;
                    case RESPONSE_SOLICITED:
                        processSolicited(p);
                        break;
                    default:
                        Log.i(TAG, mTAG + "Invalid response type: " + responseType);
                        break;
                }
            } finally {
                p.recycle();
            }
        }

        private int processSolicited(Parcel p) {
            Integer token = null;
            byte responseData[] = null;
            String stringsResponseData[] = null;
            Exception errorEx = null;

            try {
                token = p.readInt();
                int err = p.readInt();

                if (DBG) {
                    Log.i(TAG, mTAG + String.format(Locale.US, ": processSolicited() token: 0x%X err: %d", token, err));
                }

                if (err != RIL_CLIENT_ERR_SUCCESS) {
                    throw new RemoteException("remote error " + err);
                }

                responseData = p.createByteArray();
                stringsResponseData = p.createStringArray();

            } catch (Exception ex) {
               Log.e(TAG, mTAG + ex.getMessage());
                errorEx = ex;
            }

            if (token == null) {
               Log.e(TAG, mTAG + "token is null", errorEx);
            } else {
                synchronized (this) {
                    Message m = mMessages.remove(token);

                    if (m != null) {
                        switch (m.what) {
                            case ID_REQUEST_AT_COMMAND:
                            case ID_RESPONSE_AT_COMMAND:
                            case RIL_REQUEST_OEM_STRINGS:
                                m.obj = new StringsResult(stringsResponseData, errorEx);
                                m.sendToTarget();
                                break;
                            default:
                                m.obj = new RawResult(responseData, errorEx);
                                m.sendToTarget();
                        }
                    } else {
                        Log.i(TAG, mTAG + "Message with token " + token + " not found");
                    }
                }
            }
            return RIL_CLIENT_ERR_SUCCESS;
        }
    }

    public static class RemoteException extends Exception {

        public RemoteException(String detailMessage) {
            super(detailMessage);
        }
    }

}