/******************************************************************************
 * Copyright (C) Leap Motion, Inc. 2011-2017.                                 *
 * Leap Motion proprietary and  confidential.                                 *
 *                                                                            *
 * Use subject to the terms of the Leap Motion SDK Agreement available at     *
 * https://developer.leapmotion.com/sdk_agreement, or another agreement       *
 * between Leap Motion and you, your company or other organization.           *
 ******************************************************************************/

using System;
using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
using Leap.Unity.Space;
using Leap.Unity.Query;
using Leap.Unity.Attributes;

namespace Leap.Unity.GraphicalRenderer {

  [LeapGraphicTag("Text")]
  [Serializable]
  public class LeapTextRenderer : LeapRenderingMethod<LeapTextGraphic>, ISupportsAddRemove {
    public const string DEFAULT_FONT = "Arial.ttf";
    public const string DEFAULT_SHADER = "LeapMotion/GraphicRenderer/Text/Dynamic";
    public const float SCALE_CONSTANT = 0.001f;

    [EditTimeOnly, SerializeField]
    private Font _font;

    [EditTimeOnly, SerializeField]
    private float _dynamicPixelsPerUnit = 1.0f;

    [EditTimeOnly, SerializeField]
    public bool _useColor = true;

    [EditTimeOnly, SerializeField]
    public Color _globalTint = Color.white;

    [Header("Rendering Settings")]
    [EditTimeOnly, SerializeField]
    private Shader _shader;

    [EditTimeOnly, SerializeField]
    private float _scale = 1f;

    [SerializeField]
    private RendererMeshData _meshData;

    [SerializeField]
    private Material _material;

    //Curved space
    private const string CURVED_PARAMETERS = LeapGraphicRenderer.PROPERTY_PREFIX + "Curved_GraphicParameters";
    private List<Matrix4x4> _curved_worldToAnchor = new List<Matrix4x4>();
    private List<Matrix4x4> _curved_meshTransforms = new List<Matrix4x4>();
    private List<Vector4> _curved_graphicParameters = new List<Vector4>();

    public override SupportInfo GetSpaceSupportInfo(LeapSpace space) {
      return SupportInfo.FullSupport();
    }

    public void OnAddRemoveGraphics(List<int> dirtyIndexes) {
      while(_meshData.Count > group.graphics.Count) {
        _meshData.RemoveMesh(_meshData.Count - 1);
      }

      while(_meshData.Count < group.graphics.Count) {
        group.graphics[_meshData.Count].isRepresentationDirty = true;
        _meshData.AddMesh(new Mesh());
      }
    }

    public override void OnEnableRenderer() {
      foreach (var graphic in group.graphics) {
        var textGraphic = graphic as LeapTextGraphic;
        _font.RequestCharactersInTexture(textGraphic.text);
      }

      generateMaterial();

      Font.textureRebuilt += onFontTextureRebuild;
    }

    public override void OnDisableRenderer() {
      Font.textureRebuilt -= onFontTextureRebuild;
    }

    public override void OnUpdateRenderer() {
      ensureFontIsUpToDate();

      for (int i = 0; i < group.graphics.Count; i++) {
        var graphic = group.graphics[i] as LeapTextGraphic;

        if (graphic.isRepresentationDirtyOrEditTime || graphic.HasRectChanged()) {
          generateTextMesh(i, graphic, _meshData[i]);
        }
      }

      if (renderer.space == null) {
        using (new ProfilerSample("Draw Meshes")) {
          for (int i = 0; i < group.graphics.Count; i++) {
            var graphic = group.graphics[i];
            Graphics.DrawMesh(_meshData[i], graphic.transform.localToWorldMatrix, _material, 0);
          }
        }
      } else if (renderer.space is LeapRadialSpace) {
        var curvedSpace = renderer.space as LeapRadialSpace;

        using (new ProfilerSample("Build Material Data")) {
          _curved_worldToAnchor.Clear();
          _curved_meshTransforms.Clear();
          _curved_graphicParameters.Clear();
          for (int i = 0; i < _meshData.Count; i++) {
            var graphic = group.graphics[i];
            var transformer = graphic.anchor.transformer;

            Vector3 localPos = renderer.transform.InverseTransformPoint(graphic.transform.position);

            Matrix4x4 mainTransform = renderer.transform.localToWorldMatrix * transformer.GetTransformationMatrix(localPos);
            Matrix4x4 deform = renderer.transform.worldToLocalMatrix * Matrix4x4.TRS(renderer.transform.position - graphic.transform.position, Quaternion.identity, Vector3.one) * graphic.transform.localToWorldMatrix;
            Matrix4x4 total = mainTransform * deform;

            _curved_graphicParameters.Add((transformer as IRadialTransformer).GetVectorRepresentation(graphic.transform));
            _curved_meshTransforms.Add(total);
            _curved_worldToAnchor.Add(mainTransform.inverse);
          }
        }

        using (new ProfilerSample("Upload Material Data")) {
          _material.SetFloat(SpaceProperties.RADIAL_SPACE_RADIUS, curvedSpace.radius);
          _material.SetMatrixArraySafe("_GraphicRendererCurved_WorldToAnchor", _curved_worldToAnchor);
          _material.SetMatrix("_GraphicRenderer_LocalToWorld", renderer.transform.localToWorldMatrix);
          _material.SetVectorArraySafe("_GraphicRendererCurved_GraphicParameters", _curved_graphicParameters);
        }

        using (new ProfilerSample("Draw Meshes")) {
          for (int i = 0; i < _meshData.Count; i++) {
            Graphics.DrawMesh(_meshData[i], _curved_meshTransforms[i], _material, 0);
          }
        }
      }
    }

#if UNITY_EDITOR
    public override void OnEnableRendererEditor() {
      base.OnEnableRendererEditor();

      _font = Resources.GetBuiltinResource<Font>(DEFAULT_FONT);
      _shader = Shader.Find(DEFAULT_SHADER);
    }

    public override void OnUpdateRendererEditor() {
      base.OnUpdateRendererEditor();

      if (_font == null) {
        _font = Resources.GetBuiltinResource<Font>(DEFAULT_FONT);
      }

      if (_shader == null) {
        _shader = Shader.Find(DEFAULT_SHADER);
      }

      _meshData.Validate(this);

      //Make sure we have enough meshes to render all our graphics
      while (_meshData.Count > group.graphics.Count) {
        UnityEngine.Object.DestroyImmediate(_meshData[_meshData.Count - 1]);
        _meshData.RemoveMesh(_meshData.Count - 1);
      }

      while (_meshData.Count < group.graphics.Count) {
        _meshData.AddMesh(new Mesh());
      }

      generateMaterial();

      PreventDuplication(ref _material);
    }
#endif

    private void onFontTextureRebuild(Font font) {
      if (font != _font) {
        return;
      }

      foreach (var graphic in group.graphics) {
        graphic.isRepresentationDirty = true;
      }
    }

    private void generateMaterial() {
      if (_material == null) {
        _material = new Material(_font.material);
      }

#if UNITY_EDITOR
      Undo.RecordObject(_material, "Touched material");
#endif

      _material.mainTexture = _font.material.mainTexture;
      _material.name = "Font material";
      _material.shader = _shader;

      foreach (var keyword in _material.shaderKeywords) {
        _material.DisableKeyword(keyword);
      }

      if (renderer.space != null) {
        if (renderer.space is LeapCylindricalSpace) {
          _material.EnableKeyword(SpaceProperties.CYLINDRICAL_FEATURE);
        } else if (renderer.space is LeapSphericalSpace) {
          _material.EnableKeyword(SpaceProperties.SPHERICAL_FEATURE);
        }
      }

      if (_useColor) {
        _material.EnableKeyword(LeapGraphicRenderer.FEATURE_PREFIX + "VERTEX_COLORS");
      }
    }

    private void ensureFontIsUpToDate() {
      CharacterInfo info;
      bool doesNeedRebuild = false;

      for (int i = 0; i < group.graphics.Count; i++) {
        var graphic = group.graphics[i] as LeapTextGraphic;
        int scaledFontSize = Mathf.RoundToInt(graphic.fontSize * _dynamicPixelsPerUnit);

        if (graphic.isRepresentationDirtyOrEditTime) {
          for (int j = 0; j < graphic.text.Length; j++) {
            char character = graphic.text[j];

            if (!_font.GetCharacterInfo(character, out info, scaledFontSize, graphic.fontStyle)) {
              doesNeedRebuild = true;
              break;
            }
          }

          if (doesNeedRebuild) {
            break;
          }
        }
      }

      if (!doesNeedRebuild) {
        return;
      }

      for (int i = 0; i < group.graphics.Count; i++) {
        var graphic = group.graphics[i] as LeapTextGraphic;
        int scaledFontSize = Mathf.RoundToInt(graphic.fontSize * _dynamicPixelsPerUnit);

        graphic.isRepresentationDirty = true;
        _font.RequestCharactersInTexture(graphic.text,
                                         scaledFontSize,
                                         graphic.fontStyle);
      }
    }

    private List<TextWrapper.Line> _tempLines = new List<TextWrapper.Line>();
    private List<Vector3> _verts = new List<Vector3>();
    private List<Vector4> _uvs = new List<Vector4>();
    private List<Color> _colors = new List<Color>();
    private List<int> _tris = new List<int>();
    private void generateTextMesh(int index, LeapTextGraphic graphic, Mesh mesh) {
      using (new ProfilerSample("Generate Text Mesh")) {
        mesh.Clear();

        graphic.isRepresentationDirty = false;

        int scaledFontSize = Mathf.RoundToInt(graphic.fontSize * _dynamicPixelsPerUnit);

        //Check for characters not found in the font
        {
          HashSet<char> unfoundCharacters = null;
          CharacterInfo info;
          foreach (var character in graphic.text) {
            if (character == '\n') {
              continue;
            }

            if (unfoundCharacters != null && unfoundCharacters.Contains(character)) {
              continue;
            }

            if (!_font.GetCharacterInfo(character, out info, scaledFontSize, graphic.fontStyle)) {
              if (unfoundCharacters == null) unfoundCharacters = new HashSet<char>();
              unfoundCharacters.Add(character);
              Debug.LogError("Could not find character [" + character + "] in font " + _font + "!");
            }
          }
        }

        var textGraphic = graphic as LeapTextGraphic;
        var text = textGraphic.text;

        float _charScale = this._scale * SCALE_CONSTANT / _dynamicPixelsPerUnit;
        float _scale = _charScale * graphic.fontSize / _font.fontSize;
        float lineHeight = _scale * textGraphic.lineSpacing * _font.lineHeight * _dynamicPixelsPerUnit;

        RectTransform rectTransform = textGraphic.transform as RectTransform;
        float maxWidth;
        if (rectTransform != null) {
          maxWidth = rectTransform.rect.width;
        } else {
          maxWidth = float.MaxValue;
        }

        _widthCalculator.font = _font;
        _widthCalculator.charScale = _charScale;
        _widthCalculator.fontStyle = graphic.fontStyle;
        _widthCalculator.scaledFontSize = scaledFontSize;
        TextWrapper.Wrap(text, textGraphic.tokens, _tempLines, _widthCalculator.func, maxWidth);

        float textHeight = _tempLines.Count * lineHeight;

        Vector3 origin = Vector3.zero;
        origin.y -= _font.ascent * _scale * _dynamicPixelsPerUnit;

        if (rectTransform != null) {
          origin.y -= rectTransform.rect.y;

          switch (textGraphic.verticalAlignment) {
            case LeapTextGraphic.VerticalAlignment.Center:
              origin.y -= (rectTransform.rect.height - textHeight) / 2;
              break;
            case LeapTextGraphic.VerticalAlignment.Bottom:
              origin.y -= (rectTransform.rect.height - textHeight);
              break;
          }
        }

        foreach (var line in _tempLines) {

          if (rectTransform != null) {
            origin.x = rectTransform.rect.x;
            switch (textGraphic.horizontalAlignment) {
              case LeapTextGraphic.HorizontalAlignment.Center:
                origin.x += (rectTransform.rect.width - line.width) / 2;
                break;
              case LeapTextGraphic.HorizontalAlignment.Right:
                origin.x += (rectTransform.rect.width - line.width);
                break;
            }
          } else {
            switch (textGraphic.horizontalAlignment) {
              case LeapTextGraphic.HorizontalAlignment.Left:
                origin.x = 0;
                break;
              case LeapTextGraphic.HorizontalAlignment.Center:
                origin.x = -line.width / 2;
                break;
              case LeapTextGraphic.HorizontalAlignment.Right:
                origin.x = -line.width;
                break;
            }
          }

          for (int i = line.start; i < line.end; i++) {
            char c = text[i];

            CharacterInfo info;
            if (!_font.GetCharacterInfo(c, out info, scaledFontSize, graphic.fontStyle)) {
              continue;
            }

            int offset = _verts.Count;
            _tris.Add(offset + 0);
            _tris.Add(offset + 1);
            _tris.Add(offset + 2);

            _tris.Add(offset + 0);
            _tris.Add(offset + 2);
            _tris.Add(offset + 3);

            _verts.Add(_charScale * new Vector3(info.minX, info.maxY, 0) + origin);
            _verts.Add(_charScale * new Vector3(info.maxX, info.maxY, 0) + origin);
            _verts.Add(_charScale * new Vector3(info.maxX, info.minY, 0) + origin);
            _verts.Add(_charScale * new Vector3(info.minX, info.minY, 0) + origin);

            _uvs.Add(info.uvTopLeft);
            _uvs.Add(info.uvTopRight);
            _uvs.Add(info.uvBottomRight);
            _uvs.Add(info.uvBottomLeft);

            if (_useColor) {
              _colors.Append(4, _globalTint * graphic.color);
            }

            origin.x += info.advance * _charScale;
          }
          origin.y -= lineHeight;
        }

        for (int i = 0; i < _uvs.Count; i++) {
          Vector4 uv = _uvs[i];
          uv.w = index;
          _uvs[i] = uv;
        }

        mesh.SetVertices(_verts);
        mesh.SetTriangles(_tris, 0);
        mesh.SetUVs(0, _uvs);

        if (_useColor) {
          mesh.SetColors(_colors);
        }

        _verts.Clear();
        _uvs.Clear();
        _tris.Clear();
        _colors.Clear();
        _tempLines.Clear();
      }
    }

    private CharWidthCalculator _widthCalculator = new CharWidthCalculator();
    private class CharWidthCalculator {
      public Font font;
      public int scaledFontSize;
      public FontStyle fontStyle;
      public float charScale;

      public Func<char, float> func;

      public CharWidthCalculator() {
        func = funcMethod;
      }

      private float funcMethod(char c) {
        CharacterInfo info;
        font.GetCharacterInfo(c, out info, scaledFontSize, fontStyle);
        return info.advance * charScale;
      }
    }
  }
}
