package juloo.keyboard2;

import android.os.Handler;
import android.os.Message;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.NoSuchElementException;

/**
 * Manage pointers (fingers) on the screen and long presses.
 * Call back to IPointerEventHandler.
 */
public final class Pointers implements Handler.Callback
{
  public static final int FLAG_P_LATCHABLE = 1;
  public static final int FLAG_P_LATCHED = (1 << 1);
  public static final int FLAG_P_FAKE = (1 << 2);
  public static final int FLAG_P_LOCKABLE = (1 << 3);
  public static final int FLAG_P_LOCKED = (1 << 4);
  public static final int FLAG_P_SLIDING = (1 << 5);
  /** Clear latched (only if also FLAG_P_LATCHABLE set). */
  public static final int FLAG_P_CLEAR_LATCHED = (1 << 6);
  /** Can't be locked, even when long pressing. */
  public static final int FLAG_P_CANT_LOCK = (1 << 7);

  private Handler _longpress_handler;
  private ArrayList<Pointer> _ptrs = new ArrayList<Pointer>();
  private IPointerEventHandler _handler;
  private Config _config;

  public Pointers(IPointerEventHandler h, Config c)
  {
    _longpress_handler = new Handler(this);
    _handler = h;
    _config = c;
  }

  /** Return the list of modifiers currently activated. */
  public Modifiers getModifiers()
  {
    return getModifiers(false);
  }

  /** When [skip_latched] is true, don't take flags of latched keys into account. */
  private Modifiers getModifiers(boolean skip_latched)
  {
    int n_ptrs = _ptrs.size();
    KeyValue[] mods = new KeyValue[n_ptrs];
    int n_mods = 0;
    for (int i = 0; i < n_ptrs; i++)
    {
      Pointer p = _ptrs.get(i);
      if (p.value != null
          && !(skip_latched && p.hasFlagsAny(FLAG_P_LATCHED)
            && (p.flags & FLAG_P_LOCKED) == 0))
        mods[n_mods++] = p.value;
    }
    return Modifiers.ofArray(mods, n_mods);
  }

  public void clear()
  {
    for (Pointer p : _ptrs)
      stopLongPress(p);
    _ptrs.clear();
  }

  public boolean isKeyDown(KeyboardData.Key k)
  {
    for (Pointer p : _ptrs)
      if (p.key == k)
        return true;
    return false;
  }

  /** See [FLAG_P_*] flags. Returns [-1] if the key is not pressed. */
  public int getKeyFlags(KeyValue kv)
  {
    for (Pointer p : _ptrs)
      if (p.value != null && p.value.equals(kv))
        return p.flags;
    return -1;
  }

  /** The key must not be already latched . */
  void add_fake_pointer(KeyboardData.Key key, KeyValue kv, boolean locked)
  {
    Pointer ptr = new Pointer(-1, key, kv, 0.f, 0.f, Modifiers.EMPTY);
    ptr.flags = FLAG_P_FAKE | FLAG_P_LATCHED;
    if (locked)
      ptr.flags |= FLAG_P_LOCKED;
    _ptrs.add(ptr);
    _handler.onPointerFlagsChanged(false);
  }

  /** Set whether a key is latched or locked by adding a "fake" pointer, a
      pointer that is not due to user interaction.
      This is used by auto-capitalisation.

      When [lock] is true, [latched] control whether the modifier is locked or disabled.
      When [lock] is false, an existing locked pointer is not affected. */
  public void set_fake_pointer_state(KeyboardData.Key key, KeyValue kv,
      boolean latched, boolean lock)
  {
    Pointer ptr = getLatched(key, kv);
    if (ptr == null)
    {
      // No existing pointer, latch the key.
      if (latched)
        add_fake_pointer(key, kv, lock);
    }
    else if ((ptr.flags & FLAG_P_FAKE) != 0)
    {} // Key already latched but not by a fake ptr, do nothing.
    else if (lock)
    {
      // Acting on locked modifiers, replace the pointer each time.
      removePtr(ptr);
      if (latched)
        add_fake_pointer(key, kv, lock);
    }
    else if ((ptr.flags & FLAG_P_LOCKED) != 0)
    {} // Existing ptr is locked but [lock] is false, do not continue.
    else if (!latched)
    {
      // Key is latched by a fake ptr. Unlatch if requested.
      removePtr(ptr);
      _handler.onPointerFlagsChanged(false);
    }
  }

  // Receiving events

  public void onTouchUp(int pointerId)
  {
    Pointer ptr = getPtr(pointerId);
    if (ptr == null)
      return;
    if (ptr.hasFlagsAny(FLAG_P_SLIDING))
    {
      clearLatched();
      ptr.sliding.onTouchUp(ptr);
      return;
    }
    stopLongPress(ptr);
    KeyValue ptr_value = ptr.value;
    if (ptr.gesture != null && ptr.gesture.is_in_progress())
    {
      // A gesture was in progress
      ptr.gesture.pointer_up();
    }
    Pointer latched = getLatched(ptr);
    if (latched != null) // Already latched
    {
      removePtr(ptr); // Remove dupplicate
      if ((latched.flags & FLAG_P_LOCKABLE) != 0) // Toggle lockable key
        lockPointer(latched, false);
      else // Otherwise, unlatch
      {
        removePtr(latched);
        _handler.onPointerUp(ptr_value, ptr.modifiers);
      }
    }
    else if ((ptr.flags & FLAG_P_LATCHABLE) != 0)
    {
      // Latchable but non-special keys must clear latched.
      if ((ptr.flags & FLAG_P_CLEAR_LATCHED) != 0)
        clearLatched();
      ptr.flags |= FLAG_P_LATCHED;
      ptr.pointerId = -1;
      _handler.onPointerFlagsChanged(false);
    }
    else
    {
      clearLatched();
      removePtr(ptr);
      _handler.onPointerUp(ptr_value, ptr.modifiers);
    }
  }

  public void onTouchCancel()
  {
    clear();
    _handler.onPointerFlagsChanged(true);
  }

  /* Whether an other pointer is down on a non-special key. */
  private boolean isOtherPointerDown()
  {
    for (Pointer p : _ptrs)
      if (!p.hasFlagsAny(FLAG_P_LATCHED) &&
          (p.value == null || !p.value.hasFlagsAny(KeyValue.FLAG_SPECIAL)))
        return true;
    return false;
  }

  public void onTouchDown(float x, float y, int pointerId, KeyboardData.Key key)
  {
    // Ignore new presses while a sliding key is active. On some devices, ghost
    // touch events can happen while the pointer travels on top of other keys.
    if (isSliding())
      return;
    // Don't take latched modifiers into account if an other key is pressed.
    // The other key already "own" the latched modifiers and will clear them.
    Modifiers mods = getModifiers(isOtherPointerDown());
    KeyValue value = _handler.modifyKey(key.keys[0], mods);
    Pointer ptr = new Pointer(pointerId, key, value, x, y, mods);
    _ptrs.add(ptr);
    startLongPress(ptr);
    _handler.onPointerDown(value, false);
  }

  static final int[] DIRECTION_TO_INDEX = new int[]{
    7, 2, 2, 6, 6, 4, 4, 8, 8, 3, 3, 5, 5, 1, 1, 7
  };

  /**
   * [direction] is an int between [0] and [15] that represent 16 sections of a
   * circle, clockwise, starting at the top.
   */
  KeyValue getKeyAtDirection(KeyboardData.Key k, int direction)
  {
    return k.keys[DIRECTION_TO_INDEX[direction]];
  }

  /**
   * Get the key nearest to [direction] that is not key0. Take care
   * of applying [_handler.modifyKey] to the selected key in the same
   * operation to be sure to treat removed keys correctly.
   * Return [null] if no key could be found in the given direction or
   * if the selected key didn't change.
   */
  private KeyValue getNearestKeyAtDirection(Pointer ptr, int direction)
  {
    KeyValue k;
    // [i] is [0, -1, 1, -2, 2, ...]
    for (int i = 0; i > -4; i = (~i>>31) - i)
    {
      int d = (direction + i + 16) % 16;
      // Don't make the difference between a key that doesn't exist and a key
      // that is removed by [_handler]. Triggers side effects.
      k = _handler.modifyKey(getKeyAtDirection(ptr.key, d), ptr.modifiers);
      if (k != null)
        return k;
    }
    return null;
  }

  public void onTouchMove(float x, float y, int pointerId)
  {
    Pointer ptr = getPtr(pointerId);
    if (ptr == null)
      return;
    if (ptr.hasFlagsAny(FLAG_P_SLIDING))
    {
      ptr.sliding.onTouchMove(ptr, x);
      return;
    }

    // The position in a IME windows is clampled to view.
    // For a better up swipe behaviour, set the y position to a negative value when clamped.
    if (y == 0.0) y = -400;
    float dx = x - ptr.downX;
    float dy = y - ptr.downY;

    float dist = Math.abs(dx) + Math.abs(dy);
    if (dist < _config.swipe_dist_px)
    {
      // Pointer is still on the center.
      if (ptr.gesture == null || !ptr.gesture.is_in_progress())
        return;
      // Gesture ended
      ptr.gesture.moved_to_center();
      ptr.value = apply_gesture(ptr, ptr.gesture.get_gesture());
      ptr.flags = 0;

    }
    else
    { // Pointer is on a quadrant.
      // See [getKeyAtDirection()] for the meaning. The starting point on the
      // circle is the top direction.
      double a = Math.atan2(dy, dx) + Math.PI;
      // a is between 0 and 2pi, 0 is pointing to the left
      // add 12 to align 0 to the top
      int direction = ((int)(a * 8 / Math.PI) + 12) % 16;
      if (ptr.gesture == null)
      { // Gesture starts

        ptr.gesture = new Gesture(direction);
        KeyValue new_value = getNearestKeyAtDirection(ptr, direction);
        if (new_value != null)
        { // Pointer is swiping into a side key.

          ptr.value = new_value;
          ptr.flags = pointer_flags_of_kv(new_value);
          // Sliding mode is entered when key5 or key6 is down on a slider key.
          if (ptr.key.slider &&
              (new_value.equals(ptr.key.getKeyValue(5))
               || new_value.equals(ptr.key.getKeyValue(6))))
          {
            startSliding(ptr, x);
          }
          _handler.onPointerDown(new_value, true);
        }

      }
      else if (ptr.gesture.changed_direction(direction))
      { // Gesture changed state
        if (!ptr.gesture.is_in_progress())
        { // Gesture ended
          _handler.onPointerFlagsChanged(true);
        }
        else
        {
          ptr.value = apply_gesture(ptr, ptr.gesture.get_gesture());
          restartLongPress(ptr);
          ptr.flags = 0; // Special behaviors are ignored during a gesture.
        }
      }
    }
  }

  // Pointers management

  private Pointer getPtr(int pointerId)
  {
    for (Pointer p : _ptrs)
      if (p.pointerId == pointerId)
        return p;
    return null;
  }

  private void removePtr(Pointer ptr)
  {
    _ptrs.remove(ptr);
  }

  private Pointer getLatched(Pointer target)
  {
    return getLatched(target.key, target.value);
  }

  private Pointer getLatched(KeyboardData.Key k, KeyValue v)
  {
    if (v == null)
      return null;
    for (Pointer p : _ptrs)
      if (p.key == k && p.hasFlagsAny(FLAG_P_LATCHED)
          && p.value != null && p.value.equals(v))
        return p;
    return null;
  }

  private void clearLatched()
  {
    for (int i = _ptrs.size() - 1; i >= 0; i--)
    {
      Pointer ptr = _ptrs.get(i);
      // Latched and not locked, remove
      if (ptr.hasFlagsAny(FLAG_P_LATCHED) && (ptr.flags & FLAG_P_LOCKED) == 0)
        _ptrs.remove(i);
      // Not latched but pressed, don't latch once released and stop long press.
      else if ((ptr.flags & FLAG_P_LATCHABLE) != 0)
        ptr.flags &= ~FLAG_P_LATCHABLE;
    }
  }

  /** Make a pointer into the locked state. */
  private void lockPointer(Pointer ptr, boolean shouldVibrate)
  {
    ptr.flags = (ptr.flags & ~FLAG_P_LOCKABLE) | FLAG_P_LOCKED;
    _handler.onPointerFlagsChanged(shouldVibrate);
  }

  boolean isSliding()
  {
    for (Pointer ptr : _ptrs)
      if (ptr.hasFlagsAny(FLAG_P_SLIDING))
        return true;
    return false;
  }

  // Key repeat

  /** Message from [_longpress_handler]. */
  @Override
  public boolean handleMessage(Message msg)
  {
    for (Pointer ptr : _ptrs)
    {
      if (ptr.timeoutWhat == msg.what)
      {
        handleLongPress(ptr);
        return true;
      }
    }
    return false;
  }

  private static int uniqueTimeoutWhat = 0;

  private void startLongPress(Pointer ptr)
  {
    int what = (uniqueTimeoutWhat++);
    ptr.timeoutWhat = what;
    _longpress_handler.sendEmptyMessageDelayed(what, _config.longPressTimeout);
  }

  private void stopLongPress(Pointer ptr)
  {
    _longpress_handler.removeMessages(ptr.timeoutWhat);
  }

  private void restartLongPress(Pointer ptr)
  {
    stopLongPress(ptr);
    startLongPress(ptr);
  }

  /** A pointer is long pressing. */
  private void handleLongPress(Pointer ptr)
  {
    // Long press toggle lock on modifiers
    if ((ptr.flags & FLAG_P_LATCHABLE) != 0)
    {
      if (!ptr.hasFlagsAny(FLAG_P_CANT_LOCK))
        lockPointer(ptr, true);
      return;
    }
    // Latched key, no key
    if (ptr.hasFlagsAny(FLAG_P_LATCHED) || ptr.value == null)
      return;
    // Key is long-pressable
    KeyValue kv = KeyModifier.modify_long_press(ptr.value);
    if (!kv.equals(ptr.value))
    {
      ptr.value = kv;
      _handler.onPointerDown(kv, true);
      return;
    }
    // Special keys
    if (kv.hasFlagsAny(KeyValue.FLAG_SPECIAL))
      return;
    // For every other keys, key-repeat
    if (_config.keyrepeat_enabled)
    {
      _handler.onPointerHold(kv, ptr.modifiers);
      _longpress_handler.sendEmptyMessageDelayed(ptr.timeoutWhat,
          _config.longPressInterval);
    }
  }

  // Sliding

  void startSliding(Pointer ptr, float x)
  {
    stopLongPress(ptr);
    ptr.flags |= FLAG_P_SLIDING;
    ptr.sliding = new Sliding(x);
  }

  /** Return the [FLAG_P_*] flags that correspond to pressing [kv]. */
  static int pointer_flags_of_kv(KeyValue kv)
  {
    int flags = 0;
    if (kv.hasFlagsAny(KeyValue.FLAG_LATCH))
    {
      // Non-special latchable key must clear modifiers and can't be locked
      if (!kv.hasFlagsAny(KeyValue.FLAG_SPECIAL))
        flags |= FLAG_P_CLEAR_LATCHED | FLAG_P_CANT_LOCK;
      flags |= FLAG_P_LATCHABLE;
    }
    if (kv.hasFlagsAny(KeyValue.FLAG_LOCK))
      flags |= FLAG_P_LOCKABLE;
    return flags;
  }

  // Gestures

  /** Apply a gesture to the current key. */
  KeyValue apply_gesture(Pointer ptr, Gesture.Name gesture)
  {
    switch (gesture)
    {
      case None:
        return ptr.value;
      case Swipe:
        return ptr.value;
      case Roundtrip:
        return
          modify_key_with_extra_modifier(
              ptr,
              getNearestKeyAtDirection(ptr, ptr.gesture.current_direction()),
              KeyValue.Modifier.GESTURE);
      case Circle:
        return
          modify_key_with_extra_modifier(ptr, ptr.key.keys[0],
              KeyValue.Modifier.GESTURE);
      case Anticircle:
        return _handler.modifyKey(ptr.key.anticircle, ptr.modifiers);
    }
    return ptr.value; // Unreachable
  }

  KeyValue modify_key_with_extra_modifier(Pointer ptr, KeyValue kv,
      KeyValue.Modifier extra_mod)
  {
    return
      _handler.modifyKey(kv,
        ptr.modifiers.with_extra_mod(KeyValue.makeInternalModifier(extra_mod)));
  }

  // Pointers

  private static final class Pointer
  {
    /** -1 when latched. */
    public int pointerId;
    /** The Key pressed by this Pointer */
    public final KeyboardData.Key key;
    /** Gesture state, see [Gesture]. [null] means the pointer has not moved out of the center region. */
    public Gesture gesture;
    /** Selected value with [modifiers] applied. */
    public KeyValue value;
    public float downX;
    public float downY;
    /** Modifier flags at the time the key was pressed. */
    public Modifiers modifiers;
    /** See [FLAG_P_*] flags. */
    public int flags;
    /** Identify timeout messages. */
    public int timeoutWhat;
    /** [null] when not in sliding mode. */
    public Sliding sliding;

    public Pointer(int p, KeyboardData.Key k, KeyValue v, float x, float y, Modifiers m)
    {
      pointerId = p;
      key = k;
      gesture = null;
      value = v;
      downX = x;
      downY = y;
      modifiers = m;
      flags = (v == null) ? 0 : pointer_flags_of_kv(v);
      timeoutWhat = -1;
      sliding = null;
    }

    public boolean hasFlagsAny(int has)
    {
      return ((flags & has) != 0);
    }
  }

  public final class Sliding
  {
    /** Accumulated distance since last event. */
    float d = 0.f;
    /** The slider speed changes depending on the pointer speed. */
    float speed = 1.f;
    /** Coordinate of the last move. */
    float last_x;
    /** [System.currentTimeMillis()] at the time of the last move. */
    long last_move_ms;

    public Sliding(float x)
    {
      last_x = x;
      last_move_ms = System.currentTimeMillis();
    }

    static final float SPEED_SMOOTHING = 0.7f;
    /** Avoid absurdly large values. */
    static final float SPEED_MAX = 4.f;

    public void onTouchMove(Pointer ptr, float x)
    {
      d += (x - last_x) * speed / _config.slide_step_px;
      update_speed(x);
      // Send an event when [abs(d)] exceeds [1].
      int d_ = (int)d;
      if (d_ != 0)
      {
        d -= d_;
        int key_index = (d_ < 0) ? 5 : 6;
        ptr.value = _handler.modifyKey(ptr.key.keys[key_index], ptr.modifiers);
        send_key(ptr, Math.abs(d_));
      }
    }

    /** Handle a sliding pointer going up. Latched modifiers are not
        cleared to allow easy adjustments to the cursors. The pointer is
        cancelled. */
    public void onTouchUp(Pointer ptr)
    {
      removePtr(ptr);
      _handler.onPointerFlagsChanged(false);
    }

    /** Send the pressed key [n] times. */
    void send_key(Pointer ptr, int n)
    {
      if (ptr.value == null)
        return;
      // Avoid looping if possible to avoid lag while sliding fast
      KeyValue multiplied = multiply_key(ptr.value, n);
      if (multiplied != null)
        _handler.onPointerHold(multiplied, ptr.modifiers);
      else
        for (int i = 0; i < n; i++)
          _handler.onPointerHold(ptr.value, ptr.modifiers);
    }

    /** Return a key performing the same action as [kv] but [n] times. Returns
        [null] if [kv] cannot be multiplied. */
    KeyValue multiply_key(KeyValue kv, int n)
    {
      switch (kv.getKind())
      {
        case Cursor_move:
          return KeyValue.cursorMoveKey(kv.getCursorMove() * n);
      }
      return null;
    }

    /** [speed] is computed from the elapsed time and distance traveled
        between two move events. Exponential smoothing is used to smooth out
        the noise. Sets [last_move_ms] and [last_x]. */
    void update_speed(float x)
    {
      long now = System.currentTimeMillis();
      float instant_speed = Math.min(SPEED_MAX,
          Math.abs(x - last_x) / (float)(now - last_move_ms) + 1.f);
      speed = speed + (instant_speed - speed) * SPEED_SMOOTHING;
      last_move_ms = now;
      last_x = x;
    }
  }

  /** Represent modifiers currently activated.
      Sorted in the order they should be evaluated. */
  public static final class Modifiers
  {
    private final KeyValue[] _mods;
    private final int _size;

    private Modifiers(KeyValue[] m, int s)
    {
      _mods = m; _size = s;
    }

    public KeyValue get(int i) { return _mods[_size - 1 - i]; }
    public int size() { return _size; }
    public boolean has(KeyValue.Modifier m)
    {
      for (int i = 0; i < _size; i++)
      {
        KeyValue kv = _mods[i];
        switch (kv.getKind())
        {
          case Modifier:
            if (kv.getModifier().equals(m))
              return true;
        }
      }
      return false;
    }

    /** Return a copy of this object with an extra modifier added. */
    public Modifiers with_extra_mod(KeyValue m)
    {
      KeyValue[] newmods = Arrays.copyOf(_mods, _size + 1);
      newmods[_size] = m;
      return ofArray(newmods, newmods.length);
    }

    /** Returns the activated modifiers that are not in [m2]. */
    public Iterator<KeyValue> diff(Modifiers m2)
    {
      return new ModifiersDiffIterator(this, m2);
    }

    @Override
    public int hashCode() { return Arrays.hashCode(_mods); }
    @Override
    public boolean equals(Object obj)
    {
      return Arrays.equals(_mods, ((Modifiers)obj)._mods);
    }

    public static final Modifiers EMPTY =
      new Modifiers(new KeyValue[0], 0);

    protected static Modifiers ofArray(KeyValue[] mods, int size)
    {
      // Sort and remove duplicates and nulls.
      if (size > 1)
      {
        Arrays.sort(mods, 0, size);
        int j = 0;
        for (int i = 0; i < size; i++)
        {
          KeyValue m = mods[i];
          if (m != null && (i + 1 >= size || m != mods[i + 1]))
          {
            mods[j] = m;
            j++;
          }
        }
        size = j;
      }
      return new Modifiers(mods, size);
    }

    /** Returns modifiers that are in [m1_] but not in [m2_]. */
    static final class ModifiersDiffIterator
        implements Iterator<KeyValue>
    {
      Modifiers m1;
      int i1 = 0;
      Modifiers m2;
      int i2 = 0;

      public ModifiersDiffIterator(Modifiers m1_, Modifiers m2_)
      {
        m1 = m1_;
        m2 = m2_;
        advance();
      }

      public boolean hasNext()
      {
        return i1 < m1._size;
      }

      public KeyValue next()
      {
        if (i1 >= m1._size)
          throw new NoSuchElementException();
        KeyValue m = m1._mods[i1];
        i1++;
        advance();
        return m;
      }

      /** Advance to the next element if [i1] is not a valid element. The end
          is reached when [i1 = m1.size()].  */
      void advance()
      {
        while (i1 < m1.size())
        {
          KeyValue m = m1._mods[i1];
          while (true)
          {
            if (i2 >= m2._size)
              return;
            int d = m.compareTo(m2._mods[i2]);
            if (d < 0)
              return;
            i2++;
            if (d == 0)
              break;
          }
          i1++;
        }
      }
    }
  }

  public interface IPointerEventHandler
  {
    /** Key can be modified or removed by returning [null]. */
    public KeyValue modifyKey(KeyValue k, Modifiers mods);

    /** A key is pressed. [getModifiers()] is uptodate. Might be called after a
        press or a swipe to a different value. Down events are not paired with
        up events. */
    public void onPointerDown(KeyValue k, boolean isSwipe);

    /** Key is released. [k] is the key that was returned by
        [modifySelectedKey] or [modifySelectedKey]. */
    public void onPointerUp(KeyValue k, Modifiers mods);

    /** Flags changed because latched or locked keys or cancelled pointers. */
    public void onPointerFlagsChanged(boolean shouldVibrate);

    /** Key is repeating. */
    public void onPointerHold(KeyValue k, Modifiers mods);
  }
}
