package com.secspace.lib.common.views.spb;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.Shader;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.os.SystemClock;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.Interpolator;

import com.secspace.lib.common.R;

import static com.secspace.lib.common.views.spb.SmoothProgressBarUtils.checkColors;
import static com.secspace.lib.common.views.spb.SmoothProgressBarUtils.checkNotNull;
import static com.secspace.lib.common.views.spb.SmoothProgressBarUtils.checkPositive;
import static com.secspace.lib.common.views.spb.SmoothProgressBarUtils.checkPositiveOrZero;
import static com.secspace.lib.common.views.spb.SmoothProgressBarUtils.checkSpeed;


/**
 * Created by castorflex on 11/10/13.
 */
public class SmoothProgressDrawable extends Drawable implements Animatable {

  public interface Callbacks {
    public void onStop();

    public void onStart();
  }

  private static final long  FRAME_DURATION   = 1000 / 60;
  private final static float OFFSET_PER_FRAME = 0.01f;

  private final Rect fBackgroundRect = new Rect();
  private Callbacks    mCallbacks;
  private Interpolator mInterpolator;
  private Rect         mBounds;
  private Paint        mPaint;
  private int[]        mColors;
  private int          mColorsIndex;
  private boolean      mRunning;
  private float        mCurrentOffset;
  private float        mFinishingOffset;
  private int          mSeparatorLength;
  private int          mSectionsCount;
  private float        mSpeed;
  private float        mProgressiveStartSpeed;
  private float        mProgressiveStopSpeed;
  private boolean      mReversed;
  private boolean      mNewTurn;
  private boolean      mMirrorMode;
  private float        mMaxOffset;
  private boolean      mFinishing;
  private boolean      mProgressiveStartActivated;
  private int          mStartSection;
  private int          mCurrentSections;
  private float        mStrokeWidth;
  private Drawable     mBackgroundDrawable;
  private boolean      mUseGradients;
  private int[]        mLinearGradientColors;
  private float[]      mLinearGradientPositions;


  private SmoothProgressDrawable(Interpolator interpolator,
                                 int sectionsCount,
                                 int separatorLength,
                                 int[] colors,
                                 float strokeWidth,
                                 float speed,
                                 float progressiveStartSpeed,
                                 float progressiveStopSpeed,
                                 boolean reversed,
                                 boolean mirrorMode,
                                 Callbacks callbacks,
                                 boolean progressiveStartActivated,
                                 Drawable backgroundDrawable,
                                 boolean useGradients) {
    mRunning = false;
    mInterpolator = interpolator;
    mSectionsCount = sectionsCount;
    mStartSection = 0;
    mCurrentSections = mSectionsCount;
    mSeparatorLength = separatorLength;
    mSpeed = speed;
    mProgressiveStartSpeed = progressiveStartSpeed;
    mProgressiveStopSpeed = progressiveStopSpeed;
    mReversed = reversed;
    mColors = colors;
    mColorsIndex = 0;
    mMirrorMode = mirrorMode;
    mFinishing = false;
    mBackgroundDrawable = backgroundDrawable;
    mStrokeWidth = strokeWidth;

    mMaxOffset = 1f / mSectionsCount;

    mPaint = new Paint();
    mPaint.setStrokeWidth(strokeWidth);
    mPaint.setStyle(Paint.Style.STROKE);
    mPaint.setDither(false);
    mPaint.setAntiAlias(false);

    mProgressiveStartActivated = progressiveStartActivated;
    mCallbacks = callbacks;

    mUseGradients = useGradients;
    refreshLinearGradientOptions();
  }

  ////////////////////////////////////////////////////////////////////////////
  ///////////////////         SETTERS
  public void setInterpolator(Interpolator interpolator) {
    if (interpolator == null) throw new IllegalArgumentException("Interpolator cannot be null");
    mInterpolator = interpolator;
    invalidateSelf();
  }

  public void setColors(int[] colors) {
    if (colors == null || colors.length == 0)
      throw new IllegalArgumentException("Colors cannot be null or empty");
    mColorsIndex = 0;
    mColors = colors;
    refreshLinearGradientOptions();
    invalidateSelf();
  }

  public void setColor(int color) {
    setColors(new int[]{color});
  }

  public void setSpeed(float speed) {
    if (speed < 0) throw new IllegalArgumentException("Speed must be >= 0");
    mSpeed = speed;
    invalidateSelf();
  }

  public void setProgressiveStartSpeed(float speed) {
    if (speed < 0) throw new IllegalArgumentException("SpeedProgressiveStart must be >= 0");
    mProgressiveStartSpeed = speed;
    invalidateSelf();
  }

  public void setProgressiveStopSpeed(float speed) {
    if (speed < 0) throw new IllegalArgumentException("SpeedProgressiveStop must be >= 0");
    mProgressiveStopSpeed = speed;
    invalidateSelf();
  }

  public void setSectionsCount(int sectionsCount) {
    if (sectionsCount <= 0) throw new IllegalArgumentException("SectionsCount must be > 0");
    mSectionsCount = sectionsCount;
    mMaxOffset = 1f / mSectionsCount;
    mCurrentOffset %= mMaxOffset;
    refreshLinearGradientOptions();
    invalidateSelf();
  }

  public void setSeparatorLength(int separatorLength) {
    if (separatorLength < 0)
      throw new IllegalArgumentException("SeparatorLength must be >= 0");
    mSeparatorLength = separatorLength;
    invalidateSelf();
  }

  public void setStrokeWidth(float strokeWidth) {
    if (strokeWidth < 0) throw new IllegalArgumentException("The strokeWidth must be >= 0");
    mPaint.setStrokeWidth(strokeWidth);
    invalidateSelf();
  }

  public void setReversed(boolean reversed) {
    if (mReversed == reversed) return;
    mReversed = reversed;
    invalidateSelf();
  }

  public void setMirrorMode(boolean mirrorMode) {
    if (mMirrorMode == mirrorMode) return;
    mMirrorMode = mirrorMode;
    invalidateSelf();
  }

  public void setBackgroundDrawable(Drawable backgroundDrawable) {
    if (mBackgroundDrawable == backgroundDrawable) return;
    mBackgroundDrawable = backgroundDrawable;
    invalidateSelf();
  }

  public Drawable getBackgroundDrawable() {
    return mBackgroundDrawable;
  }

  public int[] getColors() {
    return mColors;
  }

  public float getStrokeWidth() {
    return mStrokeWidth;
  }

  public void setProgressiveStartActivated(boolean progressiveStartActivated) {
    mProgressiveStartActivated = progressiveStartActivated;
  }

  public void setUseGradients(boolean useGradients) {
    if (mUseGradients == useGradients) return;

    mUseGradients = useGradients;
    refreshLinearGradientOptions();
    invalidateSelf();
  }

  protected void refreshLinearGradientOptions() {
    if (mUseGradients) {
      mLinearGradientColors = new int[mSectionsCount + 2];
      mLinearGradientPositions = new float[mSectionsCount + 2];
    } else {
      mPaint.setShader(null);
      mLinearGradientColors = null;
      mLinearGradientPositions = null;
    }
  }

  ////////////////////////////////////////////////////////////////////////////
  ///////////////////         DRAW

  @Override
  public void draw(Canvas canvas) {
    mBounds = getBounds();
    canvas.clipRect(mBounds);

    //new turn
    if (mNewTurn) {
      mColorsIndex = decrementColor(mColorsIndex);
      mNewTurn = false;

      if (isFinishing()) {
        mStartSection++;

        if (mStartSection > mSectionsCount) {
          stop();
          return;
        }
      }
      if (mCurrentSections < mSectionsCount) {
        mCurrentSections++;
      }
    }

    if (mUseGradients)
      drawGradient(canvas);

    drawStrokes(canvas);
  }

  private void drawGradient(Canvas canvas) {
    float xSectionWidth = 1f / mSectionsCount;
    int currentIndexColor = mColorsIndex;

    mLinearGradientPositions[0] = 0f;
    mLinearGradientPositions[mLinearGradientPositions.length - 1] = 1f;
    int firstColorIndex = currentIndexColor - 1;
    if (firstColorIndex < 0) firstColorIndex += mColors.length;

    mLinearGradientColors[0] = mColors[firstColorIndex];

    for (int i = 0; i < mSectionsCount; ++i) {

      float position = mInterpolator.getInterpolation(i * xSectionWidth + mCurrentOffset);
      mLinearGradientPositions[i + 1] = position;
      mLinearGradientColors[i + 1] = mColors[currentIndexColor];

      currentIndexColor = (currentIndexColor + 1) % mColors.length;
    }
    mLinearGradientColors[mLinearGradientColors.length - 1] = mColors[currentIndexColor];

    float left = mReversed ? (mMirrorMode ? Math.abs(mBounds.left - mBounds.right) / 2 : mBounds.left) : mBounds.left;
    float right = mMirrorMode ? (mReversed ? mBounds.left : Math.abs(mBounds.left - mBounds.right) / 2) :
        mBounds.right;
    float top = mBounds.centerY() - mStrokeWidth / 2;
    float bottom = mBounds.centerY() + mStrokeWidth / 2;
    LinearGradient linearGradient = new LinearGradient(left, top, right, bottom,
        mLinearGradientColors, mLinearGradientPositions,
        mMirrorMode ? Shader.TileMode.MIRROR : Shader.TileMode.CLAMP);

    mPaint.setShader(linearGradient);
  }

  private void drawStrokes(Canvas canvas) {
    if (mReversed) {
      canvas.translate(mBounds.width(), 0);
      canvas.scale(-1, 1);
    }

    float prevValue = 0f;
    int boundsWidth = mBounds.width();
    if (mMirrorMode) boundsWidth /= 2;
    int width = boundsWidth + mSeparatorLength + mSectionsCount;
    int centerY = mBounds.centerY();
    float xSectionWidth = 1f / mSectionsCount;

    float startX;
    float endX;
    float firstX = 0;
    float lastX = 0;
    float prev;
    float end;
    float spaceLength;
    float xOffset;
    float ratioSectionWidth;
    float sectionWidth;
    float drawLength;
    int currentIndexColor = mColorsIndex;

    if (mStartSection == mCurrentSections && mCurrentSections == mSectionsCount) {
      firstX = canvas.getWidth();
    }

    for (int i = 0; i <= mCurrentSections; ++i) {
      xOffset = xSectionWidth * i + mCurrentOffset;
      prev = Math.max(0f, xOffset - xSectionWidth);
      ratioSectionWidth = Math.abs(mInterpolator.getInterpolation(prev) -
          mInterpolator.getInterpolation(Math.min(xOffset, 1f)));
      sectionWidth = (int) (width * ratioSectionWidth);

      if (sectionWidth + prev < width)
        spaceLength = Math.min(sectionWidth, mSeparatorLength);
      else
        spaceLength = 0f;

      drawLength = sectionWidth > spaceLength ? sectionWidth - spaceLength : 0;
      end = prevValue + drawLength;
      if (end > prevValue && i >= mStartSection) {
        float xFinishingOffset = mInterpolator.getInterpolation(Math.min(mFinishingOffset, 1f));
        startX = Math.max(xFinishingOffset * width, Math.min(boundsWidth, prevValue));
        endX = Math.min(boundsWidth, end);
        drawLine(canvas, boundsWidth, startX, centerY, endX, centerY, currentIndexColor);
        if (i == mStartSection) { // first loop
          firstX = startX - mSeparatorLength;
        }
      }
      if (i == mCurrentSections) {
        lastX = prevValue + sectionWidth; //because we want to keep the separator effect
      }

      prevValue = end + spaceLength;
      currentIndexColor = incrementColor(currentIndexColor);
    }

    drawBackgroundIfNeeded(canvas, firstX, lastX);
  }

  private void drawLine(Canvas canvas, int canvasWidth, float startX, float startY, float stopX, float stopY, int currentIndexColor) {
    mPaint.setColor(mColors[currentIndexColor]);

    if (!mMirrorMode) {
      canvas.drawLine(startX, startY, stopX, stopY, mPaint);
    } else {
      if (mReversed) {
        canvas.drawLine(canvasWidth + startX, startY, canvasWidth + stopX, stopY, mPaint);
        canvas.drawLine(canvasWidth - startX, startY, canvasWidth - stopX, stopY, mPaint);
      } else {
        canvas.drawLine(startX, startY, stopX, stopY, mPaint);
        canvas.drawLine(canvasWidth * 2 - startX, startY, canvasWidth * 2 - stopX, stopY, mPaint);
      }
    }
  }

  private void drawBackgroundIfNeeded(Canvas canvas, float firstX, float lastX) {
    if (mBackgroundDrawable == null) return;

    fBackgroundRect.top = (int) ((canvas.getHeight() - mStrokeWidth) / 2);
    fBackgroundRect.bottom = (int) ((canvas.getHeight() + mStrokeWidth) / 2);

    fBackgroundRect.left = 0;
    fBackgroundRect.right = mMirrorMode ? canvas.getWidth() / 2 : canvas.getWidth();
    mBackgroundDrawable.setBounds(fBackgroundRect);

    //draw the background if the animation is over
    if (!isRunning()) {
      if (mMirrorMode) {
        canvas.save();
        canvas.translate(canvas.getWidth() / 2, 0);
        drawBackground(canvas, 0, fBackgroundRect.width());
        canvas.scale(-1, 1);
        drawBackground(canvas, 0, fBackgroundRect.width());
        canvas.restore();
      } else {
        drawBackground(canvas, 0, fBackgroundRect.width());
      }
      return;
    }

    if (!isFinishing() && !isStarting()) return;

    if (firstX > lastX) {
      float temp = firstX;
      firstX = lastX;
      lastX = temp;
    }

    if (firstX > 0) {
      if (mMirrorMode) {
        canvas.save();
        canvas.translate(canvas.getWidth() / 2, 0);
        if (mReversed) {
          drawBackground(canvas, 0, firstX);
          canvas.scale(-1, 1);
          drawBackground(canvas, 0, firstX);
        } else {
          drawBackground(canvas, canvas.getWidth() / 2 - firstX, canvas.getWidth() / 2);
          canvas.scale(-1, 1);
          drawBackground(canvas, canvas.getWidth() / 2 - firstX, canvas.getWidth() / 2);
        }
        canvas.restore();
      } else {
        drawBackground(canvas, 0, firstX);
      }
    }
    if (lastX <= canvas.getWidth()) {
      if (mMirrorMode) {
        canvas.save();
        canvas.translate(canvas.getWidth() / 2, 0);
        if (mReversed) {
          drawBackground(canvas, lastX, canvas.getWidth() / 2);
          canvas.scale(-1, 1);
          drawBackground(canvas, lastX, canvas.getWidth() / 2);
        } else {
          drawBackground(canvas, 0, canvas.getWidth() / 2 - lastX);
          canvas.scale(-1, 1);
          drawBackground(canvas, 0, canvas.getWidth() / 2 - lastX);
        }
        canvas.restore();
      } else {
        drawBackground(canvas, lastX, canvas.getWidth());
      }
    }
  }

  private void drawBackground(Canvas canvas, float fromX, float toX) {
    int count = canvas.save();
    canvas.clipRect(fromX, (int) ((canvas.getHeight() - mStrokeWidth) / 2),
        toX, (int) ((canvas.getHeight() + mStrokeWidth) / 2));
    mBackgroundDrawable.draw(canvas);
    canvas.restoreToCount(count);
  }

  private int incrementColor(int colorIndex) {
    ++colorIndex;
    if (colorIndex >= mColors.length) colorIndex = 0;
    return colorIndex;
  }

  private int decrementColor(int colorIndex) {
    --colorIndex;
    if (colorIndex < 0) colorIndex = mColors.length - 1;
    return colorIndex;
  }

  /**
   * Start the animation with the first color.
   * Calls progressiveStart(0)
   */
  public void progressiveStart() {
    progressiveStart(0);
  }

  /**
   * Start the animation from a given color.
   *
   * @param index
   */
  public void progressiveStart(int index) {
    resetProgressiveStart(index);
    start();
  }

  private void resetProgressiveStart(int index) {
    checkColorIndex(index);

    mCurrentOffset = 0;
    mFinishing = false;
    mFinishingOffset = 0f;
    mStartSection = 0;
    mCurrentSections = 0;
    mColorsIndex = index;
  }

  /**
   * Finish the animation by animating the remaining sections.
   */
  public void progressiveStop() {
    mFinishing = true;
    mStartSection = 0;
  }

  @Override
  public void setAlpha(int alpha) {
    mPaint.setAlpha(alpha);
  }

  @Override
  public void setColorFilter(ColorFilter cf) {
    mPaint.setColorFilter(cf);
  }

  @Override
  public int getOpacity() {
    return PixelFormat.TRANSPARENT;
  }

  ///////////////////////////////////////////////////////////////////////////
  ///////////////////         Animation: based on http://cyrilmottier.com/2012/11/27/actionbar-on-the-move/
  @Override
  public void start() {
    if (mProgressiveStartActivated) {
      resetProgressiveStart(0);
    }
    if (isRunning()) return;
    if (mCallbacks != null) {
      mCallbacks.onStart();
    }
    scheduleSelf(mUpdater, SystemClock.uptimeMillis() + FRAME_DURATION);
    invalidateSelf();
  }

  @Override
  public void stop() {
    if (!isRunning()) return;
    if (mCallbacks != null) {
      mCallbacks.onStop();
    }
    mRunning = false;
    unscheduleSelf(mUpdater);
  }

  @Override
  public void scheduleSelf(Runnable what, long when) {
    mRunning = true;
    super.scheduleSelf(what, when);
  }

  @Override
  public boolean isRunning() {
    return mRunning;
  }

  public boolean isStarting() {
    return mCurrentSections < mSectionsCount;
  }

  public boolean isFinishing() {
    return mFinishing;
  }

  private final Runnable mUpdater = new Runnable() {

    @Override
    public void run() {
      if (isFinishing()) {
        mFinishingOffset += (OFFSET_PER_FRAME * mProgressiveStopSpeed);
        mCurrentOffset += (OFFSET_PER_FRAME * mProgressiveStopSpeed);
        if (mFinishingOffset >= 1f) {
          stop();
        }
      } else if (isStarting()) {
        mCurrentOffset += (OFFSET_PER_FRAME * mProgressiveStartSpeed);
      } else {
        mCurrentOffset += (OFFSET_PER_FRAME * mSpeed);
      }

      if (mCurrentOffset >= mMaxOffset) {
        mNewTurn = true;
        mCurrentOffset -= mMaxOffset;
      }

      if (isRunning())
        scheduleSelf(mUpdater, SystemClock.uptimeMillis() + FRAME_DURATION);

      invalidateSelf();
    }
  };

  ////////////////////////////////////////////////////////////////////////////
  ///////////////////     Listener

  public void setCallbacks(Callbacks callbacks) {
    mCallbacks = callbacks;
  }

  ////////////////////////////////////////////////////////////////////////////
  ///////////////////     Checks

  private void checkColorIndex(int index) {
    if (index < 0 || index >= mColors.length) {
      throw new IllegalArgumentException(String.format("Index %d not valid", index));
    }
  }

  ////////////////////////////////////////////////////////////////////////////
  ///////////////////         BUILDER

  /**
   * Builder for SmoothProgressDrawable! You must use it!
   */
  public static class Builder {
    private Interpolator mInterpolator;
    private int          mSectionsCount;
    private int[]        mColors;
    private float        mSpeed;
    private float        mProgressiveStartSpeed;
    private float        mProgressiveStopSpeed;
    private boolean      mReversed;
    private boolean      mMirrorMode;
    private float        mStrokeWidth;
    private int          mStrokeSeparatorLength;
    private boolean      mProgressiveStartActivated;
    private boolean      mGenerateBackgroundUsingColors;
    private boolean      mGradients;
    private Drawable     mBackgroundDrawableWhenHidden;

    private Callbacks mOnProgressiveStopEndedListener;

    public Builder(Context context) {
      this(context, false);
    }

    public Builder(Context context, boolean editMode) {
      initValues(context, editMode);
    }

    public SmoothProgressDrawable build() {
      if (mGenerateBackgroundUsingColors) {
        mBackgroundDrawableWhenHidden = SmoothProgressBarUtils.generateDrawableWithColors(mColors, mStrokeWidth);
      }
      SmoothProgressDrawable ret = new SmoothProgressDrawable(
          mInterpolator,
          mSectionsCount,
          mStrokeSeparatorLength,
          mColors,
          mStrokeWidth,
          mSpeed,
          mProgressiveStartSpeed,
          mProgressiveStopSpeed,
          mReversed,
          mMirrorMode,
          mOnProgressiveStopEndedListener,
          mProgressiveStartActivated,
          mBackgroundDrawableWhenHidden,
          mGradients);
      return ret;
    }

    private void initValues(Context context, boolean editMode) {
      Resources res = context.getResources();
      mInterpolator = new AccelerateInterpolator();
      if (!editMode) {
        mSectionsCount = res.getInteger(R.integer.spb_default_sections_count);
        mSpeed = Float.parseFloat(res.getString(R.string.spb_default_speed));
        mReversed = res.getBoolean(R.bool.spb_default_reversed);
        mProgressiveStartActivated = res.getBoolean(R.bool.spb_default_progressiveStart_activated);
        mColors = new int[]{res.getColor(R.color.spb_default_color)};
        mStrokeSeparatorLength = res.getDimensionPixelSize(R.dimen.spb_default_stroke_separator_length);
        mStrokeWidth = res.getDimensionPixelOffset(R.dimen.spb_default_stroke_width);
      } else {
        mSectionsCount = 4;
        mSpeed = 1f;
        mReversed = false;
        mProgressiveStartActivated = false;
        mColors = new int[]{0xff33b5e5};
        mStrokeSeparatorLength = 4;
        mStrokeWidth = 4;
      }
      mProgressiveStartSpeed = mSpeed;
      mProgressiveStopSpeed = mSpeed;
      mGradients = false;
    }

    public Builder interpolator(Interpolator interpolator) {
      checkNotNull(interpolator, "Interpolator");
      mInterpolator = interpolator;
      return this;
    }

    public Builder sectionsCount(int sectionsCount) {
      checkPositive(sectionsCount, "Sections count");
      mSectionsCount = sectionsCount;
      return this;
    }

    public Builder separatorLength(int separatorLength) {
      checkPositiveOrZero(separatorLength, "Separator length");
      mStrokeSeparatorLength = separatorLength;
      return this;
    }

    public Builder color(int color) {
      mColors = new int[]{color};
      return this;
    }

    public Builder colors(int[] colors) {
      checkColors(colors);
      mColors = colors;
      return this;
    }

    public Builder strokeWidth(float width) {
      checkPositiveOrZero(width, "Width");
      mStrokeWidth = width;
      return this;
    }

    public Builder speed(float speed) {
      checkSpeed(speed);
      mSpeed = speed;
      return this;
    }

    public Builder progressiveStartSpeed(float progressiveStartSpeed) {
      checkSpeed(progressiveStartSpeed);
      mProgressiveStartSpeed = progressiveStartSpeed;
      return this;
    }

    public Builder progressiveStopSpeed(float progressiveStopSpeed) {
      checkSpeed(progressiveStopSpeed);
      mProgressiveStopSpeed = progressiveStopSpeed;
      return this;
    }

    public Builder reversed(boolean reversed) {
      mReversed = reversed;
      return this;
    }

    public Builder mirrorMode(boolean mirrorMode) {
      mMirrorMode = mirrorMode;
      return this;
    }

    public Builder progressiveStart(boolean progressiveStartActivated) {
      mProgressiveStartActivated = progressiveStartActivated;
      return this;
    }

    public Builder callbacks(Callbacks onProgressiveStopEndedListener) {
      mOnProgressiveStopEndedListener = onProgressiveStopEndedListener;
      return this;
    }

    public Builder backgroundDrawable(Drawable backgroundDrawableWhenHidden) {
      mBackgroundDrawableWhenHidden = backgroundDrawableWhenHidden;
      return this;
    }

    public Builder generateBackgroundUsingColors() {
      mGenerateBackgroundUsingColors = true;
      return this;
    }

    public Builder gradients() {
      return gradients(true);
    }

    public Builder gradients(boolean useGradients) {
      mGradients = useGradients;
      return this;
    }
  }
}
