/**
 * Copyright 2017 Google Inc. All Rights Reserved.
 *
 * 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 ai.api.ui;

import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.LinearInterpolator;
import android.view.animation.Transformation;

import java.util.List;

import ai.api.android.AIConfiguration;
import ai.api.AIListener;
import ai.api.android.AIService;
import ai.api.AIServiceException;
import ai.api.PartialResultsListener;
import ai.api.R;
import ai.api.RequestExtras;
import ai.api.model.AIError;
import ai.api.model.AIRequest;
import ai.api.model.AIResponse;
import ai.api.services.GoogleRecognitionServiceImpl;

public class AIButton extends SoundLevelButton implements AIListener {

    public interface AIButtonListener {
        void onResult(final AIResponse result);
        void onError(final AIError error);
        void onCancelled();
    }

    private static final String TAG = AIButton.class.getName();

    protected static final int[] STATE_WAITING = {R.attr.state_waiting};
    protected static final int[] STATE_SPEAKING = {R.attr.state_speaking};
    protected static final int[] STATE_INITIALIZING_TTS = {R.attr.state_initializing_tts};

    private float animationStage = 0;
    private boolean animationSecondPhase = false;
    private final WaitingAnimation animation = new WaitingAnimation();

    private AIService aiService;
    private AIButtonListener resultsListener;
    private PartialResultsListener partialResultsListener;

    @Override
    public void onResult(final AIResponse result) {
        post(new Runnable() {
            @Override
            public void run() {
                changeState(MicState.normal);
            }
        });

        if (resultsListener != null) {
            resultsListener.onResult(result);
        }
    }

    @Override
    public void onError(final AIError error) {
        post(new Runnable() {
            @Override
            public void run() {
                changeState(MicState.normal);
            }
        });

        if (resultsListener != null) {
            resultsListener.onError(error);
        }
    }

    @Override
    public void onAudioLevel(final float level) {
        setSoundLevel(level);
    }

    @Override
    public void onListeningStarted() {
        post(new Runnable() {
            @Override
            public void run() {
                changeState(MicState.listening);
            }
        });
    }

    @Override
    public void onListeningCanceled() {
        post(new Runnable() {
            @Override
            public void run() {
                changeState(MicState.normal);
            }
        });

        if (resultsListener != null) {
            resultsListener.onCancelled();
        }
    }

    @Override
    public void onListeningFinished() {
        post(new Runnable() {
            @Override
            public void run() {
                changeState(MicState.busy);
            }
        });
    }

    public enum MicState {
        normal,
        busy, // transitive state with disabled mic
        listening, // state with sound indicator
        speaking,
        initializingTts;

        public static MicState fromAttrs(final TypedArray viewAttrs) {
            if (viewAttrs.getBoolean(R.styleable.SoundLevelButton_state_listening, false))
                return listening;
            if (viewAttrs.getBoolean(R.styleable.SoundLevelButton_state_waiting, false))
                return busy;
            if (viewAttrs.getBoolean(R.styleable.SoundLevelButton_state_speaking, false))
                return speaking;
            if (viewAttrs.getBoolean(R.styleable.SoundLevelButton_state_initializing_tts, false))
                return initializingTts;

            return normal;
        }
    }

    private volatile MicState currentState = MicState.normal;

    public AIButton(final Context context) {
        super(context);
        init(context, null);
    }

    public AIButton(final Context context, final AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }

    public AIButton(final Context context, final AttributeSet attrs, final int defStyle) {
        super(context, attrs, defStyle);
        init(context, attrs);
    }

    private void init(final Context context, final AttributeSet attrs) {
        if (attrs != null) {
            final TypedArray viewAttrs = context.obtainStyledAttributes(attrs, R.styleable.SoundLevelButton);
            try {
                currentState = MicState.fromAttrs(viewAttrs);
            } finally {
                viewAttrs.recycle();
            }
        }
    }

    public void initialize(final AIConfiguration config) {
        aiService = AIService.getService(getContext(), config);
        aiService.setListener(this);

        if (aiService instanceof GoogleRecognitionServiceImpl) {
            ((GoogleRecognitionServiceImpl) aiService).setPartialResultsListener(new PartialResultsListener() {
                @Override
                public void onPartialResults(final List<String> partialResults) {
                    if (partialResultsListener != null) {
                        partialResultsListener.onPartialResults(partialResults);
                    }
                }
            });
        }
    }

    public void setResultsListener(final AIButtonListener resultsListener) {
        this.resultsListener = resultsListener;
    }

    public void setPartialResultsListener(final PartialResultsListener partialResultsListener) {
        this.partialResultsListener = partialResultsListener;
    }

    public void startListening() {
        startListening(null);
    }

    public void startListening(final RequestExtras requestExtras) {
        if (aiService != null) {
            if (currentState == MicState.normal) {
                aiService.startListening(requestExtras);
            }
        } else {
            throw new IllegalStateException("Call initialize method before usage");
        }
    }

    public AIResponse textRequest(final AIRequest request) throws AIServiceException {
        if (aiService != null) {
            return aiService.textRequest(request);
        } else {
            throw new IllegalStateException("Call initialize method before usage");
        }
    }

    public AIResponse textRequest(final String request) throws AIServiceException {
        return textRequest(new AIRequest(request));
    }

    /**
     * Get AIService object for making different data requests
     * @return
     */
    public AIService getAIService() {
        return aiService;
    }

    @Override
    protected void onClick(final View v) {
        super.onClick(v);

        if (aiService != null) {
            switch (currentState) {
                case normal:
                    aiService.startListening();
                    break;
                case busy:
                    aiService.cancel();
                    break;
                default:
                    aiService.stopListening();
                    break;
            }
        }

    }

    @Override
    public int[] onCreateDrawableState(final int extraSpace) {
        final int[] state = super.onCreateDrawableState(extraSpace + 1);
        if (currentState != null) {
            switch (currentState) {
                case normal:
                    break;
                case busy:
                    mergeDrawableStates(state, STATE_WAITING);
                    break;
                case listening:
                    mergeDrawableStates(state, STATE_LISTENING);
                    break;
                case speaking:
                    mergeDrawableStates(state, STATE_SPEAKING);
                    break;
                case initializingTts:
                    mergeDrawableStates(state, STATE_INITIALIZING_TTS);
                    break;
            }
        }
        return state;
    }

    public void resume() {
        if (aiService != null) {
            aiService.resume();
        }
    }

    public void pause() {
        cancelListening();
        if (aiService != null) {
            aiService.pause();
        }
    }

    private void cancelListening() {
        if (aiService != null) {
            if (currentState != MicState.normal) {
                aiService.cancel();
                changeState(MicState.normal);
            }
        }
    }

    protected void changeState(final MicState toState) {
        switch (toState) {
            case normal:
                stopProcessingAnimation();
                setDrawSoundLevel(false);
                break;
            case busy:
                startProcessingAnimation();
                setDrawSoundLevel(false);
                break;
            case listening:
                stopProcessingAnimation();
                setDrawSoundLevel(true);
                break;
            case speaking:
                stopProcessingAnimation();
                setDrawSoundLevel(false);
                break;
            case initializingTts:
                stopProcessingAnimation();
                setDrawSoundLevel(false);
                break;
        }

        currentState = toState;
        refreshDrawableState();
    }

    protected MicState getCurrentState() {
        return currentState;
    }

    private void startProcessingAnimation() {
        setDrawCenter(true);
        animationSecondPhase = false;
        startAnimation(animation);
    }

    private void stopProcessingAnimation() {
        setDrawCenter(false);
        clearAnimation();
        animationStage = 0;
        animationSecondPhase = false;
        postInvalidate();
    }

    @Override
    protected String getDebugState() {
        return super.getDebugState() + "\nst:" + currentState;
    }

    private class WaitingAnimation extends Animation {
        protected WaitingAnimation() {
            super();
            setDuration(1500);
            this.setRepeatCount(INFINITE);
            this.setRepeatMode(RESTART);
            this.setInterpolator(new LinearInterpolator());
        }

        @Override
        protected void applyTransformation(final float interpolatedTime, final Transformation t) {
            animationStage = interpolatedTime;
            invalidate();
        }
    }

    @Override
    protected void onDraw(final Canvas canvas) {
        super.onDraw(canvas);
        if (animationStage > 0 || animationSecondPhase) {
            final float center = getWidth() / 2f;
            final float radius = getMinRadius() * 1.25f;
            final RectF size = new RectF(center - radius, center - radius, center + radius, center + radius);
            final Paint paint = new Paint();
            paint.setColor(getResources().getColor(R.color.icon_orange_color));
            paint.setStyle(Paint.Style.STROKE);
            paint.setStrokeWidth(dpToPixels(this.getContext(), 4));
            paint.setStrokeCap(Paint.Cap.ROUND);
            paint.setAntiAlias(true);
            final float startingAngle;
            final float sweepAngle;
            if (animationStage < 0.5 && !animationSecondPhase) {
                startingAngle = 0;
                sweepAngle = animationStage * 360;
            } else {
                startingAngle = (animationStage - 0.5f) * 360;
                sweepAngle = 180;
                animationSecondPhase = true;
            }
            canvas.drawArc(size, 270f + startingAngle, sweepAngle, false, paint);
        }
    }

    private static int dpToPixels(final Context context, final float dp) {
        final Resources r = context.getResources();
        return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, r.getDisplayMetrics()));
    }
}