# -*- encoding: utf-8 -*-

# Module to "normalize" tags, meaning that when
# in the DSL file there are cases like this
#
# [a][b]text[/a][/b]
#
# we'll try to reorder the tags so that
# there are no intersecting tags:
#
# [a][b]text[/b][/a]
#
# Without this, the HTML generated by the dsl2mobi script
# will be not properly structured, which would lead to
# formatting artifacts.
#
# Typical usage:
#
# Normalizer::norm_tags("[a][b]text[/a][/b]")

KCODE='u'

module Normalizer
  extend self

  class Lexer
    def initialize(str)
      @str = str.split(//u)
      @pos = 0
      @state = :text
    end

    def get_next
      if (@pos >= @str.size)
        return [:end, ""]
      end

      value = ""
      state = :text
      @str[@pos..-1].each { |ch|
        case ch
        when "["
          break if (state == :text && !value.empty?)
          state = :tag
          @pos += 1
        when "]"
          if state == :tag
            @pos += 1
            break
          else
           $stderr.puts "ERROR: unmatched right bracket at pos #{@pos}, state #{state}, string #{@str}"
           exit 1
          end
        else
          value << ch
          @pos += 1
        end
      }
      [state, value]
    end

    def break_into_lexems
    lexems = []
      begin
        lexem = get_next
        lexems << lexem
      end while (lexem[0] != :end)
      lexems
    end
  end

  def get_prev_cluster(ary, idx)
    return nil if (idx >= ary.length || idx < 0)

    i = idx

    # skip current cluster
    while ( i >= 0 && ary[i][0] == :tag)
      i -= 1
    end

    return nil if i < 0

    # skip text
    while ( i >= 0 && ary[i][0] != :tag)
      i -= 1
    end

    return nil if i < 0

    j = i
    while ( i >= 0 && ary[i][0] == :tag)
      if i >= 1
        i -= 1
      else
        break
      end
    end

    [ary[i..j], i]
  end

  def get_cluster(ary, idx)
    return [] if (idx >= ary.length || idx < 0)

    return [] if (ary[idx][0] != :tag)

    # backwards
    i = idx
    while ( (i-1) >= 0 && ary[i-1][0] == :tag)
      i -= 1
    end

    # forwards
    j = idx
    while ( (j+1) < ary.length && ary[j+1][0] == :tag)
      j += 1
    end

    [ary[i..j], i]
  end

  def transform(ary)

    my_elem = nil
    my_idx  = nil

    ary_idx = 0

    while (ary_idx < ary.length)
      elem = ary[ary_idx]

      # jump to the next closing tag
      if elem[0] == :tag && elem[1] =~ /^\//
        cluster2, idx2 = get_cluster(ary, ary_idx)
        cluster1, idx1 = get_prev_cluster(ary, ary_idx)

        if (cluster1.nil?)
          break
        end

        cluster1_pre = cluster1.dup
        cluster2_pre = cluster2.dup

        sort_clusters(cluster1, cluster2)

        if (cluster1_pre == cluster1 && cluster2_pre == cluster2)
          # nothing has changed! move on.
          ary_idx += 1
          next
        end

        (0...cluster2.length).each { |idx|
          ary[idx2+idx] = cluster2[idx]
        }

        (0...cluster1.length).each { |idx|
          ary[idx1+idx] = cluster1[idx]
        }

      else
        ary_idx += 1
      end
    end

    ary
  end

  def to_string(ary)
    val = ""
    ary.each { |elem|
      case elem[0]
      when :tag
        val << "[#{elem[1]}]"
      when :text
        val << "#{elem[1]}"
      when :end
      end
    }
    val
  end

  def norm_tags(str)
    l = Lexer.new(str)
    lexems = l.break_into_lexems
    to_string(transform(lexems))
  end

  def matching_tag(elem, backward)
    if (backward)
      [:tag, elem[1].sub(/^\//, '')]
    else
      [:tag, "/#{elem[1].split(/\s+/)[0]}"]
    end
  end

  def sort_clusters_internal(ary1, ary2, backward)
    ary2.sort! { |x, y|
      x1 = ary1.index(matching_tag(x, backward)) || ( backward ? -1 : 1000)
      y1 = ary1.index(matching_tag(y, backward)) || ( backward ? -1 : 1000)

      if (backward)
        if (x[0] == :tag && x[1] =~ /^[^\/]/)
          x1 = -1000
        end
        if (y[0] == :tag && y[1] =~ /^[^\/]/)
          y1 = -1000
        end
      else
        # TODO: need to do something here?
      end

      -x1 <=> -y1
    }
  end

  def sort_clusters(ary1, ary2)
    sort_clusters_internal(ary1, ary2, true)
    sort_clusters_internal(ary2, ary1, false)

    ary1.each { |elem|
      idx = ary2.index(matching_tag(elem, false))
      if (idx)
        elem[0] = :text
        elem[1] = "[#{elem[1]}]"
        ary2[idx][0] = :text
        ary2[idx][1] = "[#{ary2[idx][1]}]"
      end
    }

    ary2.each { |elem|
      idx = ary1.index(matching_tag(elem, true))
      if (idx)
        elem[0] = :text
        elem[1] = "[#{elem[1]}]"
        ary1[idx][0] = :text
        ary1[idx][1] = "[#{ary1[idx][1]}]"
      end
    }
  end
end

if __FILE__ == $0
  require 'test/unit'
  class TC_MyTest < Test::Unit::TestCase
    include Normalizer

    def test_sort
      ary1 = [[:tag, "a"], [:tag, "b"]]
      ary2 = [[:tag, "/a"], [:tag, "/b"]]
      sort_clusters(ary1, ary2)
      assert_equal([:text, "[/b]"], ary2[0])
      assert_equal([:text, "[/a]"], ary2[1])
    end

    def test_sort_0
      ary1 = [[:tag, "a"],]
      ary2 = [[:tag, "/b"], [:tag, "/a"]]
      sort_clusters(ary1, ary2)
      assert_equal([:text, "[/a]"], ary2[0])
      assert_equal([:tag, "/b"], ary2[1])
    end

    def test_sort_1
      ary1 = [[:tag, "m"], [:tag, "p"], [:tag, "i"], [:tag, "c"], [:tag, "trn"], [:tag, "com"]]
      ary2 = [[:tag, "/p"]]
      sort_clusters(ary1, ary2)
      assert_equal([:text, "[/p]"], ary2[0])
    end

    def test_sort_2
      ary1 = [[:tag, "m"], [:tag, "com"], [:tag, "i"], [:tag, "trn"], [:tag, "c"]]
      exp1 = ary1.dup
      ary2 = [[:tag, "p"]]
      exp2 = ary2.dup
      sort_clusters(ary1, ary2)
      assert_equal(exp1, ary1)
      assert_equal(exp2, ary2)
    end

    def test_sort_3
      ary1 = [[:tag, "p"]]
      exp1 = [[:text, "[p]"]]
      ary2 = [[:tag, "/com"], [:tag, "/c"], [:tag, "/i"], [:tag, "/p"], [:tag, "/m"]]
      exp2 = [[:text, "[/p]"], [:tag, "/i"], [:tag, "/c"], [:tag, "/com"], [:tag, "/m"]]
      sort_clusters(ary1, ary2)
      assert_equal(exp1, ary1)
      assert_equal(exp2, ary2)
    end

    def test_sort_4
      ary1 = [[:tag, "m"], [:tag, "com"], [:tag, "i"], [:tag, "c"]]
      exp1 = [[:text, "[m]"], [:text, "[com]"], [:text, "[i]"], [:text, "[c]"]]
      ary2 = [[:tag, "/com"], [:tag, "/c"], [:tag, "/i"], [:tag, "/m"]]
      exp2 = [[:text, "[/c]"], [:text, "[/i]"], [:text, "[/com]"], [:text, "[/m]"]]
      sort_clusters(ary1, ary2)
      assert_equal(exp1, ary1)
      assert_equal(exp2, ary2)
    end

    def test_get_prev_cluster_oob
      ary =[[:tag, "a"]]
      assert_equal(nil, get_prev_cluster(ary, 1))
      assert_equal(nil, get_prev_cluster(ary, 10))
    end

    def test_get_prev_cluster
      ary = [[:tag, "a"], [:tag, "b"], [:text, "vvs"], [:tag, "b"], [:tag, "a"]]
      res = get_prev_cluster(ary, 3)
      assert_equal([[[:tag, "a"], [:tag, "b"]], 0], res)
      res = get_prev_cluster(ary, 0)
      assert_equal(nil, res)

      assert_equal([[[:tag, "a"]], 0], get_prev_cluster([[:tag, "a"], [:text, "vvs"], [:tag, "/a"]], 2))
    end

    def test_get_cluster_oob
      ary =[[:tag, "a"]]
      assert_equal([], get_cluster(ary, 1))
      assert_equal([], get_cluster(ary, 10))
    end

    def test_get_cluster
      ary = [[:tag, "a"], [:tag, "b"], [:text, "vvs"], [:tag, "b"], [:tag, "a"]]
      res = get_cluster(ary, 4)
      assert_equal([[[:tag, "b"], [:tag, "a"]], 3], res)
      assert_equal([], get_cluster(ary, 2))
    end

    TDATA = []
    TDATA << ["[p][i]n.[/i][/p][i][c],[/c][/i] [p][i]f.[/i][/p]",
             "[p][i]n.[/i][/p][i][c],[/c][/i] [p][i]f.[/i][/p]"]
    TDATA << ["[c][a]t1[/a][/c][b]t2[/b]",
              "[c][a]t1[b][/a][/c]t2[/b]"]
    TDATA <<  ["[c saddlebrown]text1[/c]",
               "[c saddlebrown]text1[/c]"]
    TDATA <<  ["[a]t1[b stuff]t2[c]t3[/c][/b][/a]",
               "[a]t1[b stuff]t2[c]t3[/a][/b][/c]"]
    TDATA << ["[c][c][x]text[/x][/c]",
              "[c][x][c]text[/c][/x]"]
    TDATA <<  ["[a][x][y][b stuff][c]t1[/c]t2[/b]t3[/y][/x][/a]",
               "[a][x][b stuff][y][c]t1[/c]t2[/b]t3[/a][/x][/y]"]
    TDATA << ["test", "test"]
    TDATA << ["привет", "привет"]
    TDATA << ["[a]привет[/a]", "[a]привет[/a]"]
    TDATA << ["[a]абц[/a][b]фгх[/b]", "[a]абц[b][/a]фгх[/b]"]
    TDATA << ["[a][b]test2[/b][/a]", "[a][b]test2[/a][/b]"]
    TDATA << ["[m][com][c][i][p]pril.[/p], [p]upotr. chasto[/p][/i][/c][/com][/m]",
              "[m][p][i][c][com]pril.[/p], [p]upotr. chasto[/com][/c][/i][/p][/m]"]

    TDATA << [ "[a]T1[c]T2[b]T3[/b][/c]T4[/a]",
               "[a]T1[c]T2[b]T3[/c][/b]T4[/a]"]

    TDATA << [ "[a][b][c]текст1[/c][/b]текст2[c]текст3[/c][/a]",
               "[b][a][c]текст1[/b][/c]текст2[c]текст3[/c][/a]"]

    TDATA << [ "[m][i][c][com]test1, [p]test 2 test 3[/p][/com][/c][/i][/m]",
               "[m][i][c][com]test1, [p]test 2 test 3[/com][/c][/i][/p][/m]"]
    count = 0
    TDATA.each { |data|
      define_method "test_#{count}" do
        assert_equal(data[0], norm_tags(data[1]))
      end
      count += 1
    }

  end
end
