/*
 * Copyright (C) 2014 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.testingcamera2;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Set;

import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.Surface;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.CompoundButton;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.ToggleButton;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCaptureSession;
import android.hardware.camera2.CameraCaptureSession.CaptureCallback;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.CaptureResult;
import android.hardware.camera2.CaptureFailure;
import android.hardware.camera2.TotalCaptureResult;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import com.android.testingcamera2.PaneTracker.PaneEvent;

import java.io.IOException;

/**
 *
 * Basic control pane block for the control list
 *
 */
public class CameraControlPane extends ControlPane {

    // XML attributes

    /** Name of pane tag */
    private static final String PANE_NAME = "camera_pane";

    /** Attribute: ID for pane (integer) */
    private static final String PANE_ID = "id";
    /** Attribute: ID for camera to select (String) */
    private static final String CAMERA_ID = "camera_id";

    // End XML attributes

    private static final int MAX_CACHED_RESULTS = 100;

    private static int mCameraPaneIdCounter = 0;

    /**
     * These correspond to the callbacks from
     * android.hardware.camera2.CameraDevice.StateCallback, plus UNAVAILABLE for
     * when there's not a valid camera selected.
     */
    private enum CameraState {
        UNAVAILABLE,
        CLOSED,
        OPENED,
        DISCONNECTED,
        ERROR
    }

    /**
     * These correspond to the callbacks from {@link CameraCaptureSession.StateCallback}, plus
     * {@code CONFIGURING} for before a session is returned and {@code NONE} for when there
     * is no session created.
     */
    private enum SessionState {
        NONE,
        CONFIGURED,
        CONFIGURE_FAILED,
        READY,
        ACTIVE,
        CLOSED
    }

    private enum CameraCall {
        NONE,
        CONFIGURE
    }

    private final int mPaneId;

    private CameraOps2 mCameraOps;
    private InfoDisplayer mInfoDisplayer;

    private Spinner mCameraSpinner;
    private ToggleButton mOpenButton;
    private Button mInfoButton;
    private TextView mStatusText;
    private Button mConfigureButton;
    private Button mStopButton;
    private Button mFlushButton;

    /**
     * All controls that should be enabled when there's a valid camera ID
     * selected
     */
    private final Set<View> mBaseControls = new HashSet<View>();
    /**
     * All controls that should be enabled when camera is at least in the OPEN
     * state
     */
    private final Set<View> mOpenControls = new HashSet<View>();
    /**
     * All controls that should be enabled when camera is at least in the IDLE
     * state
     */
    private final Set<View> mConfiguredControls = new HashSet<View>();

    private String[] mCameraIds;
    private String mCurrentCameraId;

    private CameraState mCameraState;
    private CameraDevice mCurrentCamera;
    private CameraCaptureSession mCurrentCaptureSession;
    private SessionState mSessionState = SessionState.NONE;
    private CameraCall mActiveCameraCall;
    private LinkedList<TotalCaptureResult> mRecentResults = new LinkedList<>();

    private List<Surface> mConfiguredSurfaces;
    private List<TargetControlPane> mConfiguredTargetPanes;

    /**
     * Constructor for tooling only
     */
    public CameraControlPane(Context context, AttributeSet attrs) {
        super(context, attrs, null, null);

        mPaneId = 0;
        setUpUI(context);
    }

    public CameraControlPane(TestingCamera21 tc, AttributeSet attrs, StatusListener listener) {

        super(tc, attrs, listener, tc.getPaneTracker());

        mPaneId = mCameraPaneIdCounter++;
        setUpUI(tc);
        initializeCameras(tc);

        if (mCameraIds != null) {
            switchToCamera(mCameraIds[0]);
        }
    }

    public CameraControlPane(TestingCamera21 tc, XmlPullParser configParser, StatusListener listener)
            throws XmlPullParserException, IOException {
        super(tc, null, listener, tc.getPaneTracker());

        configParser.require(XmlPullParser.START_TAG, XmlPullParser.NO_NAMESPACE, PANE_NAME);

        int paneId = getAttributeInt(configParser, PANE_ID, -1);
        if (paneId == -1) {
            mPaneId = mCameraPaneIdCounter++;
        } else {
            mPaneId = paneId;
            if (mPaneId >= mCameraPaneIdCounter) {
                mCameraPaneIdCounter = mPaneId + 1;
            }
        }

        String cameraId = getAttributeString(configParser, CAMERA_ID, null);

        configParser.next();
        configParser.require(XmlPullParser.END_TAG, XmlPullParser.NO_NAMESPACE, PANE_NAME);

        setUpUI(tc);
        initializeCameras(tc);

        boolean gotCamera = false;
        if (mCameraIds != null && cameraId != null) {
            for (int i = 0; i < mCameraIds.length; i++) {
                if (cameraId.equals(mCameraIds[i])) {
                    switchToCamera(mCameraIds[i]);
                    mCameraSpinner.setSelection(i);
                    gotCamera = true;
                }
            }
        }

        if (!gotCamera && mCameraIds != null) {
            switchToCamera(mCameraIds[0]);
        }
    }

    @Override
    public void remove() {
        closeCurrentCamera();
        super.remove();
    }

    /**
     * Get list of target panes that are currently actively configured for this
     * camera
     */
    public List<TargetControlPane> getCurrentConfiguredTargets() {
        return mConfiguredTargetPanes;
    }

    /**
     * Interface to be implemented by an application service for displaying a
     * camera's information.
     */
    public interface InfoDisplayer {
        public void showCameraInfo(String cameraId);
    }

    public CameraCharacteristics getCharacteristics() {
        if (mCurrentCameraId != null) {
            return mCameraOps.getCameraInfo(mCurrentCameraId);
        }
        return null;
    }

    public CaptureRequest.Builder getRequestBuilder(int template) {
        CaptureRequest.Builder request = null;
        if (mCurrentCamera != null) {
            try {
                request = mCurrentCamera.createCaptureRequest(template);
                // Workaround for b/15748139
                request.set(CaptureRequest.STATISTICS_LENS_SHADING_MAP_MODE,
                        CaptureRequest.STATISTICS_LENS_SHADING_MAP_MODE_ON);
            } catch (CameraAccessException e) {
                TLog.e("Unable to build request for camera %s with template %d.", e,
                        mCurrentCameraId, template);
            }
        }
        return request;
    }

    /**
     * Send single capture to camera device.
     *
     * @param request
     * @return true if capture sent successfully
     */
    public boolean capture(CaptureRequest request) {
        if (mCurrentCaptureSession != null) {
            try {
                mCurrentCaptureSession.capture(request, mResultListener, null);
                return true;
            } catch (CameraAccessException e) {
                TLog.e("Unable to capture for camera %s.", e, mCurrentCameraId);
            }
        }
        return false;
    }

    public boolean repeat(CaptureRequest request) {
        if (mCurrentCaptureSession != null) {
            try {
                mCurrentCaptureSession.setRepeatingRequest(request, mResultListener, null);
                return true;
            } catch (CameraAccessException e) {
                TLog.e("Unable to set repeating request for camera %s.", e, mCurrentCameraId);
            }
        }
        return false;
    }

    public TotalCaptureResult getResultAt(long timestamp) {
        for (TotalCaptureResult result : mRecentResults) {
            long resultTimestamp = result.get(CaptureResult.SENSOR_TIMESTAMP);
            if (resultTimestamp == timestamp) return result;
            if (resultTimestamp > timestamp) return null;
        }
        return null;
    }

    public void prepareSurface(Surface target) {
        if (mCurrentCaptureSession != null) {
            try {
                TLog.i("Preparing Surface " + target);
                mCurrentCaptureSession.prepare(target);
            } catch (CameraAccessException e) {
                TLog.e("Unable to prepare surface for camera %s.", e, mCurrentCameraId);
            } catch (IllegalArgumentException e) {
                TLog.e("Bad Surface passed to prepare", e);
            }
        }
    }

    private CaptureCallback mResultListener = new CaptureCallback() {
        @Override
        public void onCaptureStarted(CameraCaptureSession session,
                CaptureRequest request, long timestamp, long frameNumber) {
        }

        @Override
        public void onCaptureProgressed(CameraCaptureSession session,
                CaptureRequest request, CaptureResult partialResult) {
        }

        @Override
        public void onCaptureCompleted(
                CameraCaptureSession session,
                CaptureRequest request,
                TotalCaptureResult result) {
            mRecentResults.add(result);
            if (mRecentResults.size() > MAX_CACHED_RESULTS) {
                mRecentResults.remove();
            }
        }

        @Override
        public void onCaptureFailed(CameraCaptureSession session,
                CaptureRequest request, CaptureFailure failure) {
            TLog.e("Capture failed for request " + request +
                    " on frame  " + failure.getFrameNumber() + ": Reason " + failure.getReason() +
                    ". Images captured: " + failure.wasImageCaptured());
        }

        @Override
        public void onCaptureSequenceCompleted(CameraCaptureSession session,
                int sequenceId, long frameNumber) {
        }

        @Override
        public void onCaptureSequenceAborted(CameraCaptureSession session,
                int sequenceId) {
        }

        @Override
        public void onCaptureBufferLost(CameraCaptureSession session,
                CaptureRequest request, Surface target, long frameNumber) {
            TLog.e("Lost buffer for Surface " + target + " for request " + request +
                    " on frame " + frameNumber);
        }

    };

    private void setUpUI(Context context) {
        String paneName =
                String.format(Locale.US, "%s %c",
                        context.getResources().getString(R.string.camera_pane_title),
                        (char) ('A' + mPaneId));
        this.setName(paneName);

        LayoutInflater inflater =
                (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

        inflater.inflate(R.layout.camera_pane, this);

        mCameraSpinner = (Spinner) findViewById(R.id.camera_pane_camera_spinner);
        mCameraSpinner.setOnItemSelectedListener(mCameraSpinnerListener);

        mOpenButton = (ToggleButton) findViewById(R.id.camera_pane_open_button);
        mOpenButton.setOnCheckedChangeListener(mOpenButtonListener);
        mBaseControls.add(mOpenButton);

        mInfoButton = (Button) findViewById(R.id.camera_pane_info_button);
        mInfoButton.setOnClickListener(mInfoButtonListener);
        mBaseControls.add(mInfoButton);

        mStatusText = (TextView) findViewById(R.id.camera_pane_status_text);

        mConfigureButton = (Button) findViewById(R.id.camera_pane_configure_button);
        mConfigureButton.setOnClickListener(mConfigureButtonListener);
        mOpenControls.add(mConfigureButton);

        mStopButton = (Button) findViewById(R.id.camera_pane_stop_button);
        mStopButton.setOnClickListener(mStopButtonListener);
        mConfiguredControls.add(mStopButton);
        mFlushButton = (Button) findViewById(R.id.camera_pane_flush_button);
        mFlushButton.setOnClickListener(mFlushButtonListener);
        mConfiguredControls.add(mFlushButton);
    }

    private void initializeCameras(TestingCamera21 tc) {
        mCameraOps = tc.getCameraOps();
        mInfoDisplayer = tc;

        updateCameraList();
    }

    private void updateCameraList() {
        mCameraIds = null;
        try {
            mCameraIds = mCameraOps.getCamerasAndListen(mCameraAvailabilityCallback);
            String[] cameraSpinnerItems = new String[mCameraIds.length];
            for (int i = 0; i < mCameraIds.length; i++) {
                cameraSpinnerItems[i] = String.format("Camera %s", mCameraIds[i]);
            }
            mCameraSpinner.setAdapter(new ArrayAdapter<String>(getContext(), R.layout.spinner_item,
                    cameraSpinnerItems));

        } catch (CameraAccessException e) {
            TLog.e("Exception trying to get list of cameras: " + e);
        }
    }

    private final CompoundButton.OnCheckedChangeListener mOpenButtonListener =
            new CompoundButton.OnCheckedChangeListener() {
                @Override
                public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                    if (isChecked) {
                        // Open camera
                        mCurrentCamera = null;
                        mCameraOps.openCamera(mCurrentCameraId, mCameraListener);
                    } else {
                        // Close camera
                        closeCurrentCamera();
                    }
                }
            };

    private final OnClickListener mInfoButtonListener = new OnClickListener() {
        @Override
        public void onClick(View v) {
            mInfoDisplayer.showCameraInfo(mCurrentCameraId);
        }
    };

    private final OnClickListener mStopButtonListener = new OnClickListener() {
        @Override
        public void onClick(View v) {
            if (mCurrentCaptureSession != null) {
                try {
                    mCurrentCaptureSession.stopRepeating();
                } catch (CameraAccessException e) {
                    TLog.e("Unable to stop repeating request for camera %s.", e, mCurrentCameraId);
                }
            }
        }
    };

    private final OnClickListener mFlushButtonListener = new OnClickListener() {
        @Override
        public void onClick(View v) {
            if (mCurrentCaptureSession != null) {
                try {
                    mCurrentCaptureSession.abortCaptures();
                } catch (CameraAccessException e) {
                    TLog.e("Unable to flush camera %s.", e, mCurrentCameraId);
                }
            }
        }
    };

    private final OnClickListener mConfigureButtonListener = new OnClickListener() {
        @Override
        public void onClick(View v) {
            List<Surface> targetSurfaces = new ArrayList<Surface>();
            List<TargetControlPane> targetPanes = new ArrayList<TargetControlPane>();
            for (TargetControlPane targetPane : mPaneTracker.getPanes(TargetControlPane.class)) {
                Surface target = targetPane.getTargetSurfaceForCameraPane(getPaneName());
                if (target != null) {
                    targetSurfaces.add(target);
                    targetPanes.add(targetPane);
                }
            }
            try {
                TLog.i("Configuring camera %s with %d surfaces", mCurrentCamera.getId(),
                        targetSurfaces.size());
                mActiveCameraCall = CameraCall.CONFIGURE;
                if (targetSurfaces.size() > 0) {
                    mCurrentCamera.createCaptureSession(targetSurfaces, mSessionListener, /*handler*/null);
                } else if (mCurrentCaptureSession != null) {
                    mCurrentCaptureSession.close();
                    mCurrentCaptureSession = null;
                }
                mConfiguredSurfaces = targetSurfaces;
                mConfiguredTargetPanes = targetPanes;
            } catch (CameraAccessException e) {
                mActiveCameraCall = CameraCall.NONE;
                TLog.e("Unable to configure camera %s.", e, mCurrentCamera.getId());
            } catch (IllegalArgumentException e) {
                mActiveCameraCall = CameraCall.NONE;
                TLog.e("Unable to configure camera %s.", e, mCurrentCamera.getId());
            } catch (IllegalStateException e) {
                mActiveCameraCall = CameraCall.NONE;
                TLog.e("Unable to configure camera %s.", e, mCurrentCamera.getId());
            }
        }
    };

    private final CameraCaptureSession.StateCallback mSessionListener =
            new CameraCaptureSession.StateCallback() {

        @Override
        public void onConfigured(CameraCaptureSession session) {
            mCurrentCaptureSession = session;
            TLog.i("Configuration completed for camera %s.", mCurrentCamera.getId());

            setSessionState(SessionState.CONFIGURED);
        }

        @Override
        public void onConfigureFailed(CameraCaptureSession session) {
            mActiveCameraCall = CameraCall.NONE;
            TLog.e("Configuration failed for camera %s.", mCurrentCamera.getId());

            setSessionState(SessionState.CONFIGURE_FAILED);
        }

        @Override
        public void onReady(CameraCaptureSession session) {
            setSessionState(SessionState.READY);
        }

        /**
         * This method is called when the session starts actively processing capture requests.
         *
         * <p>If capture requests are submitted prior to {@link #onConfigured} being called,
         * then the session will start processing those requests immediately after the callback,
         * and this method will be immediately called after {@link #onConfigured}.
         *
         * <p>If the session runs out of capture requests to process and calls {@link #onReady},
         * then this callback will be invoked again once new requests are submitted for capture.</p>
         */
        @Override
        public void onActive(CameraCaptureSession session) {
            setSessionState(SessionState.ACTIVE);
        }

        /**
         * This method is called when the session is closed.
         *
         * <p>A session is closed when a new session is created by the parent camera device,
         * or when the parent camera device is closed (either by the user closing the device,
         * or due to a camera device disconnection or fatal error).</p>
         *
         * <p>Once a session is closed, all methods on it will throw an IllegalStateException, and
         * any repeating requests or bursts are stopped (as if {@link #stopRepeating()} was called).
         * However, any in-progress capture requests submitted to the session will be completed
         * as normal.</p>
         */
        @Override
        public void onClosed(CameraCaptureSession session) {
            // Ignore closes if the session has been replaced
            if (mCurrentCaptureSession != null && session != mCurrentCaptureSession) {
                return;
            }
            setSessionState(SessionState.CLOSED);
        }

        @Override
        public void onSurfacePrepared(CameraCaptureSession session, Surface surface) {
            TLog.i("Surface preparation complete for Surface " + surface);
        }

    };

    private final CameraDevice.StateCallback mCameraListener = new CameraDevice.StateCallback() {
        @Override
        public void onClosed(CameraDevice camera) {
            // Don't change state on close, tracked by callers of close()
            mOpenButton.setChecked(false);
        }

        @Override
        public void onDisconnected(CameraDevice camera) {
            setCameraState(CameraState.DISCONNECTED);
        }

        @Override
        public void onError(CameraDevice camera, int error) {
            setCameraState(CameraState.ERROR);
        }

        @Override
        public void onOpened(CameraDevice camera) {
            mCurrentCamera = camera;
            setCameraState(CameraState.OPENED);
        }
    };

    private void switchToCamera(String newCameraId) {
        closeCurrentCamera();

        mCurrentCameraId = newCameraId;

        if (mCurrentCameraId == null) {
            setCameraState(CameraState.UNAVAILABLE);
        } else {
            setCameraState(CameraState.CLOSED);
        }

        mPaneTracker.notifyOtherPanes(this, PaneTracker.PaneEvent.NEW_CAMERA_SELECTED);
    }

    private void closeCurrentCamera() {
        if (mCurrentCamera != null) {
            mCurrentCamera.close();
            mCurrentCamera = null;
            setCameraState(CameraState.CLOSED);
        }
    }

    private void setSessionState(SessionState newState) {
        mSessionState = newState;
        mStatusText.setText("S." + mSessionState.toString());

        switch (mSessionState) {
            case CONFIGURE_FAILED:
                mActiveCameraCall = CameraCall.NONE;
                // fall-through
            case CLOSED:
                enableBaseControls(true);
                enableOpenControls(true);
                enableConfiguredControls(false);
                mConfiguredTargetPanes = null;
                break;
            case NONE:
                enableBaseControls(true);
                enableOpenControls(true);
                enableConfiguredControls(false);
                mConfiguredTargetPanes = null;
                break;
            case CONFIGURED:
                if (mActiveCameraCall != CameraCall.CONFIGURE) {
                    throw new AssertionError();
                }
                mPaneTracker.notifyOtherPanes(this, PaneEvent.CAMERA_CONFIGURED);
                mActiveCameraCall = CameraCall.NONE;
                // fall-through
            case READY:
            case ACTIVE:
                enableBaseControls(true);
                enableOpenControls(true);
                enableConfiguredControls(true);
                break;
            default:
                throw new AssertionError("Unhandled case " + mSessionState);
        }
    }

    private void setCameraState(CameraState newState) {
        mCameraState = newState;
        mStatusText.setText("C." + mCameraState.toString());
        switch (mCameraState) {
            case UNAVAILABLE:
                enableBaseControls(false);
                enableOpenControls(false);
                enableConfiguredControls(false);
                mConfiguredTargetPanes = null;
                break;
            case CLOSED:
            case DISCONNECTED:
            case ERROR:
                enableBaseControls(true);
                enableOpenControls(false);
                enableConfiguredControls(false);
                mConfiguredTargetPanes = null;
                break;
            case OPENED:
                enableBaseControls(true);
                enableOpenControls(true);
                enableConfiguredControls(false);
                mConfiguredTargetPanes = null;
                break;
        }
    }

    private void enableBaseControls(boolean enabled) {
        for (View v : mBaseControls) {
            v.setEnabled(enabled);
        }
        if (!enabled) {
            mOpenButton.setChecked(false);
        }
    }

    private void enableOpenControls(boolean enabled) {
        for (View v : mOpenControls) {
            v.setEnabled(enabled);
        }
    }

    private void enableConfiguredControls(boolean enabled) {
        for (View v : mConfiguredControls) {
            v.setEnabled(enabled);
        }
    }

    private final CameraManager.AvailabilityCallback mCameraAvailabilityCallback =
            new CameraManager.AvailabilityCallback() {
        @Override
        public void onCameraAvailable(String cameraId) {
            // TODO: Update camera list in an intelligent fashion
            // (can't just call updateCameraList or the selected camera may change)
        }

        @Override
        public void onCameraUnavailable(String cameraId) {
            // TODO: Update camera list in an intelligent fashion
            // (can't just call updateCameraList or the selected camera may change)
        }
    };

    private final OnItemSelectedListener mCameraSpinnerListener = new OnItemSelectedListener() {
        @Override
        public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
            String newCameraId = mCameraIds[pos];
            if (newCameraId != mCurrentCameraId) {
                switchToCamera(newCameraId);
            }
        }

        @Override
        public void onNothingSelected(AdapterView<?> parent) {
            switchToCamera(null);
        }
    };
}
