# frozen_string_literal: true

module ActionDispatch
  # :stopdoc:
  module Journey
    class Format
      ESCAPE_PATH    = ->(value) { Router::Utils.escape_path(value) }
      ESCAPE_SEGMENT = ->(value) { Router::Utils.escape_segment(value) }

      Parameter = Struct.new(:name, :escaper) do
        def escape(value); escaper.call value; end
      end

      def self.required_path(symbol)
        Parameter.new symbol, ESCAPE_PATH
      end

      def self.required_segment(symbol)
        Parameter.new symbol, ESCAPE_SEGMENT
      end

      def initialize(parts)
        @parts      = parts
        @children   = []
        @parameters = []

        parts.each_with_index do |object, i|
          case object
          when Journey::Format
            @children << i
          when Parameter
            @parameters << i
          end
        end
      end

      def evaluate(hash)
        parts = @parts.dup

        @parameters.each do |index|
          param = parts[index]
          value = hash[param.name]
          return "" unless value
          parts[index] = param.escape value
        end

        @children.each { |index| parts[index] = parts[index].evaluate(hash) }

        parts.join
      end
    end

    module Visitors # :nodoc:
      class Visitor # :nodoc:
        DISPATCH_CACHE = {}

        def accept(node)
          visit(node)
        end

        private

          def visit(node)
            send(DISPATCH_CACHE[node.type], node)
          end

          def binary(node)
            visit(node.left)
            visit(node.right)
          end
          def visit_CAT(n); binary(n); end

          def nary(node)
            node.children.each { |c| visit(c) }
          end
          def visit_OR(n); nary(n); end

          def unary(node)
            visit(node.left)
          end
          def visit_GROUP(n); unary(n); end
          def visit_STAR(n); unary(n); end

          def terminal(node); end
          def visit_LITERAL(n); terminal(n); end
          def visit_SYMBOL(n);  terminal(n); end
          def visit_SLASH(n);   terminal(n); end
          def visit_DOT(n);     terminal(n); end

          private_instance_methods(false).each do |pim|
            next unless pim =~ /^visit_(.*)$/
            DISPATCH_CACHE[$1.to_sym] = pim
          end
      end

      class FunctionalVisitor # :nodoc:
        DISPATCH_CACHE = {}

        def accept(node, seed)
          visit(node, seed)
        end

        def visit(node, seed)
          send(DISPATCH_CACHE[node.type], node, seed)
        end

        def binary(node, seed)
          visit(node.right, visit(node.left, seed))
        end
        def visit_CAT(n, seed); binary(n, seed); end

        def nary(node, seed)
          node.children.inject(seed) { |s, c| visit(c, s) }
        end
        def visit_OR(n, seed); nary(n, seed); end

        def unary(node, seed)
          visit(node.left, seed)
        end
        def visit_GROUP(n, seed); unary(n, seed); end
        def visit_STAR(n, seed); unary(n, seed); end

        def terminal(node, seed);   seed; end
        def visit_LITERAL(n, seed); terminal(n, seed); end
        def visit_SYMBOL(n, seed);  terminal(n, seed); end
        def visit_SLASH(n, seed);   terminal(n, seed); end
        def visit_DOT(n, seed);     terminal(n, seed); end

        instance_methods(false).each do |pim|
          next unless pim =~ /^visit_(.*)$/
          DISPATCH_CACHE[$1.to_sym] = pim
        end
      end

      class FormatBuilder < Visitor # :nodoc:
        def accept(node); Journey::Format.new(super); end
        def terminal(node); [node.left]; end

        def binary(node)
          visit(node.left) + visit(node.right)
        end

        def visit_GROUP(n); [Journey::Format.new(unary(n))]; end

        def visit_STAR(n)
          [Journey::Format.required_path(n.left.to_sym)]
        end

        def visit_SYMBOL(n)
          symbol = n.to_sym
          if symbol == :controller
            [Journey::Format.required_path(symbol)]
          else
            [Journey::Format.required_segment(symbol)]
          end
        end
      end

      # Loop through the requirements AST.
      class Each < FunctionalVisitor # :nodoc:
        def visit(node, block)
          block.call(node)
          super
        end

        INSTANCE = new
      end

      class String < FunctionalVisitor # :nodoc:
        private

          def binary(node, seed)
            visit(node.right, visit(node.left, seed))
          end

          def nary(node, seed)
            last_child = node.children.last
            node.children.inject(seed) { |s, c|
              string = visit(c, s)
              string << "|" unless last_child == c
              string
            }
          end

          def terminal(node, seed)
            seed + node.left
          end

          def visit_GROUP(node, seed)
            visit(node.left, seed.dup << "(") << ")"
          end

          INSTANCE = new
      end

      class Dot < FunctionalVisitor # :nodoc:
        def initialize
          @nodes = []
          @edges = []
        end

        def accept(node, seed = [[], []])
          super
          nodes, edges = seed
          <<-eodot
  digraph parse_tree {
    size="8,5"
    node [shape = none];
    edge [dir = none];
    #{nodes.join "\n"}
    #{edges.join("\n")}
  }
          eodot
        end

        private

          def binary(node, seed)
            seed.last.concat node.children.map { |c|
              "#{node.object_id} -> #{c.object_id};"
            }
            super
          end

          def nary(node, seed)
            seed.last.concat node.children.map { |c|
              "#{node.object_id} -> #{c.object_id};"
            }
            super
          end

          def unary(node, seed)
            seed.last << "#{node.object_id} -> #{node.left.object_id};"
            super
          end

          def visit_GROUP(node, seed)
            seed.first << "#{node.object_id} [label=\"()\"];"
            super
          end

          def visit_CAT(node, seed)
            seed.first << "#{node.object_id} [label=\"○\"];"
            super
          end

          def visit_STAR(node, seed)
            seed.first << "#{node.object_id} [label=\"*\"];"
            super
          end

          def visit_OR(node, seed)
            seed.first << "#{node.object_id} [label=\"|\"];"
            super
          end

          def terminal(node, seed)
            value = node.left

            seed.first << "#{node.object_id} [label=\"#{value}\"];"
            seed
          end
          INSTANCE = new
      end
    end
  end
  # :startdoc:
end
