# frozen_string_literal: true

# rubocop:disable Style/PerlBackrefs -- to simplify

module Junoser
  class Ruler
    OFFSET = '    '

    def initialize(input)
      @rule = input
    end

    def to_rule
      +rule_header << rule << rule_footer
    end

    def rule
      str = @rule.read
      str = remove_comments(str)
      str = process_reserved_element(str)
      str.split("\n").map { |l| fmt(process_line(l)) }.join("\n")
    end

    private

    def remove_comments(str)
      str.gsub(%r{\s*/\*.*\*/}, '')
    end

    def process_line(str)
      return str if str =~ /^(.* do|end)$/

      str.gsub!(/("[^"]+")/) { "str(#{$1})" } # "foo" -> str("foo")

      str.gsub!(/^(\s*)arg(\.as\(:\S+\))? \($/) { "#{$1}b(arg#{$2}," } # arg ( -> b(arg,
      # str("foo") bar -> a(str("foo"), bar)
      str.gsub!(/^(\s*)(str\(\S+\)) ([^ \t\n\r\f(|,]+)(\.as\(:\S+\))?(,?)$/) do
        "#{$1}a(#{$2}, #{$3})#{$4}#{$5}"
      end
      # str("foo") (a | b) -> a(str("foo"), a | b)
      str.gsub!(/^(\s*)(str\(\S+\)) (enum)?\((.*)\)(,?)$/) do
        "#{$1}a(#{$2}, #{$3}#{$4})#{$5}"
      end
      str.gsub!(/^(\s*)(str\(\S+\)) \($/) { "#{$1}b(#{$2}," } # str("foo") ( -> b(str("foo"),
      str.gsub!(/^(\s*)(enum)?(\(.*\))(\.as\(:\S\))? \($/) { "#{$1}b(#{$2}#{$3}#{$4}," } # (a | b) ( -> b((a | b),
      # str("foo") bar ( -> b(a(str("foo"), bar),
      str.gsub!(/^(\s*)(str\(\S+\)) ([^ \t\n\r\f(|,]+) \($/) do
        "#{$1}b(a(#{$2}, #{$3}),"
      end
      # str("foo") (a | b) ( -> a(str("foo"), a | b,
      str.gsub!(/^(\s*)(str\(\S+\)) (enum)?\((.*)\) \($/) do
        "#{$1}a(#{$2}, #{$3}#{$4},"
      end
      str
    end

    def process_reserved_element(str)
      str.gsub!(/"\$\S+"/, 'arg')

      str.gsub!(/"groups" \(\s*s\(\s*any\s*\)\s*\)/, <<-REPLACE.strip)
          b(a("groups", arg),
            c(
              configuration,
              "when" (
                c(
                  "chassis" arg,
                  "member" arg,
                  "model" arg,
                  "node" arg,
                  "peers" arg,
                  "routing-engine" arg,
                  "time" (
                    c(
                      "to" arg,
                      arg
                    )
                  )
                )
              )
            )
          )
      REPLACE

      str.gsub! '"equal-literal"', '"="'
      str.gsub! '"plus-literal"', '"+"'
      str.gsub! '"minus-literal"', '"-"'

      #
      # Statements can be quoted
      #
      str.gsub!(/("ssh-\S+") arg/) { "#{$1} (quote | arg)" }
      str.gsub! '"message" arg', '"message" (quote | arg)'
      str.gsub! '"description" arg', '"description" (quote | arg)'
      str.gsub! '"as-path-prepend" arg', '"as-path-prepend" (quote | arg)'
      str.gsub! '"tcp-flags" arg', '"tcp-flags" (quote | arg)'

      str.gsub!(/^(\s*)"as-path" arg \(\s*c\(\s*arg/) do
        fmt(['"as-path" arg (',
             '  c(',
             '    quote | arg'], $1)
      end

      str.gsub!(/^rule\(:regular_expression\) do\s.*?\s*end/) do
        <<~REPLACE
          rule(:regular_expression) do
            (quote | arg).as(:arg)
          end
        REPLACE
      end

      str.gsub!(/^rule\(:login_user_object\) do\s*arg\.as\(:arg\) \(\s*c\(\s*"full-name" arg,/) do
        <<~REPLACE
          rule(:login_user_object) do
            arg.as(:arg) (
              sc(
                "full-name" (quote | arg),
        REPLACE
      end

      str.gsub!(/^(\s*)"location" arg,\s*"contact" arg,/) do
        fmt(['"location" (quote | arg),',
             '"contact" (quote | arg),'], $1)
      end

      str.gsub!(/^(\s*)"as-path" \(\s*c\(\s*"path" arg/) do
        fmt(['"as-path" (',
             '  c(',
             '    "path" (quote | arg)'], $1)
      end

      str.gsub!(/^(\s*)prefix_list_items,\s*"apply-path" arg/) do
        fmt(['"apply-path" (quote | arg),',
             'prefix_list_items'], $1)
      end

      str.gsub!(/^(\s*)"drop-profile-map" \(\s*s\(\s*"loss-priority" \(\s*(.*\s*\),)\s*"protocol" \(\s*(.*\s*\),)\s*c\(\s*"drop-profile" (.*)/) do
        fmt([
              '"drop-profile-map" (',
              '  s(',
              '    s("loss-priority",',
              "      #{$2}",
              '    s("protocol",',
              "      #{$3}",
              '    s("drop-profile",',
              "      #{$4}"
            ], $1)
      end

      #
      # "arg" matches anything so move to the end
      #
      str.gsub!(/arg \| (".*")/) { "#{$1} | arg" }
      str.gsub!(/^(\s*)c\(\s*arg,$/) { "#{$1}ca(" }
      str.gsub!(/(rule\(:control_route_filter_type\) do\s*)s\(\s*arg,/) { "#{$1}b(" }
      str.gsub!(/(rule\(:control_source_address_filter_type\) do\s*)s\(\s*arg,/) { "#{$1}b(" }
      str.gsub!(/^(rule\(:trace_file_type\) do\s*)ca\(/) { "#{$1}sca(" }

      str.gsub!(/^(rule\(:archive_object\) do\s*)c\(/) { "#{$1}sc(" }
      str.gsub!(/^(rule\(:server_group_type\) do\s*)c\(\s*c\(\s*arg\s*\)\s*\)/) { "#{$1}s(arg, arg)" }

      str.gsub!(/^(rule\(:rib_group_inet_type\) do)\s*c\(\s*arg/) do
        fmt([$1,
             '  ca(',
             '    a(arg, arg)'], '')
      end

      # Fix overkill
      str.gsub!(/^(\s*)"priority" \(\s*ca\(\s*arg\s*\)/) do
        fmt(['"priority" (',
             '  a(arg, arg)', $1])
      end

      #
      # Longer pattern first
      #
      # covers:
      #   "inet" | "inet6"
      #
      #   and even
      #
      #   "inet",
      #   "inet6"
      str.gsub!(/"cspf"(.*\s*.*)"cspf-link"/) { %("cspf-link"#{$1}"cspf") }
      str.gsub!(/"http"(.*\s*.*)"https"/) { %("https"#{$1}"http") }
      str.gsub!(/"inet"(.*\s*.*)"inet6"/) { %("inet6"#{$1}"inet") }
      str.gsub!(/"icmp"(.*\s*.*)"icmp6"/) { %("icmp6"#{$1}"icmp") }
      str.gsub!(/"icmp"(.*\s*.*)"icmpv6"/) { %("icmpv6"#{$1}"icmp") }
      str.gsub!(/"snmp"(.*\s*.*)"snmptrap"/) { %("snmptrap"#{$1}"snmp") }
      str.gsub!(/"ospf"(.*\s*.*)"ospf3"/) { %("ospf3"#{$1}"ospf") }
      str.gsub!(/"deny"(.*\s*.*)"deny-password"/) { %("deny-password"#{$1}"deny") }
      str.gsub!(/"no-redirects"(.*\s*.*)"no-redirects-ipv6"/) { %("no-redirects-ipv6"#{$1}"no-redirects") }
      str.gsub!(/"chassis"([^()]*)"chassis-ha-reswatch"/m) { %("chassis-ha-reswatch"#{$1}"chassis") }
      str.gsub! '"tls1" | "tls11" | "tls12"', '"tls11" | "tls12" | "tls1"'
      str.gsub!(/("group1" \| "group2" \| "group5") \| ([^)]+)/) { "#{$2} | #{$1}" }

      %w[ccc ethernet-over-atm tcc vpls bridge].each do |encap|
        str.gsub!(/"ethernet"(.*)"ethernet-#{encap}"/) { %("ethernet-#{encap}"#{$1}"ethernet") }
      end

      str.gsub!(/^(\s*)"path" arg \(\s*c\(\s*sc\(\s*"abstract",\s*c\(\s*"loose",\s*"loose-link",\s*"strict"\s*\)\s*\)\.as\(:oneline\)/) do
        fmt(['"path" arg (',
             '  c(',
             '    b(',
             '      ipaddr,',
             '      c(',
             '        "abstract",',
             '        c(',
             '          "loose-link",',
             '          "loose",',
             '          "strict"',
             '        )',
             '      ).as(:oneline)',
             '    )', $1])
      end

      #
      # Fix .xsd: Elements without "nokeyword" flag
      #
      str.gsub!(/\((.*) \| "name"\)/) { "(#{$1} | arg)" }
      str.gsub! '"vlan" ("all" | "vlan-name")', '"vlan" ("all" | arg)'
      str.gsub!(/\((.*) \| "vlan-id"\)/) { "(#{$1} | arg)" }

      %w[filename].each do |key|
        str.gsub! %("#{key}" arg), 'arg'
      end

      # "filename" fix above leaves "arg". Move to the end
      str.gsub!(/^(rule\(:esp_trace_file_type\) do\s*)c\(\s*arg,/) { "#{$1}ca(" }

      # Fix .xsd: system processes dhcp is valid on some platforms
      str.gsub! '"dhcp-service" (', '("dhcp-service" | "dhcp") ('

      # Fix .xsd: "icmpv6" is also acceptable
      str.gsub! '"icmp6" |', '"icmp6" | "icmpv6" |'

      # Fix .xsd: "end-range" of "member-range"
      str.gsub! '"end-range"', '"to"'

      #
      # Fix .xsd: "arg" is missing
      #
      str.gsub!(/"route-filter" (\(\s*control_route_filter_type\s*\))/) { %["route-filter" arg #{$1}.as(:oneline)] }
      str.gsub!(/"source-address-filter" (\(\s*control_source_address_filter_type\s*\))/) do
        %["source-adress-filter" arg #{$1}.as(:oneline)]
      end
      %w[file].each do |key|
        str.gsub!(/^(\s*"#{key}" \(\s*)c\(\s*arg,/) { "#{$1}sca(" }
      end

      # Fix .xsd: Unnecessary "arg" is added
      %w[exact longer orlonger].each do |key|
        str.gsub!(/^(\s*"#{key}") arg/) { $1.to_s }
      end

      # Fix .xsd: "ieee-802.3ad" is invalid
      str.gsub! '"ieee-802.3ad"', '"802.3ad"'

      # Fix .xsd: "classifiers"
      str.gsub! '("default")', '("default" | arg)'

      # Fix .xsd: "class-of-service interfaces xxx unit x"
      str.gsub! '"*"', 'arg'

      # Fix .xsd: "from-zone" arg is also required
      str.gsub!(/^        "policy" \(\s*s\(\s*arg,\s*"to-zone-name" arg,\s*(.*?)\s*\)\s*^        \),/m) do
        fmt(['b(', '  s("from-zone", arg, "to-zone", arg),', "      #{$1}", '),'], '        ')
      end

      # Fix .xsd: "members" accepts [ foo bar ]
      str.gsub! '"members" arg', '"members" any'

      # Fix .xsd: "term_object" accepts multiple conditions
      str.gsub!(/^(rule\(:term_object\) do\s*arg\.as\(:arg\) \(\s*)c\(/) { "#{$1}sc(" }

      # Fix .xsd: keywords "dest-nat-rule-match", "src-nat-rule-match", "static-nat-rule-match" are wrong
      str.gsub!(/"(dest|src|static)-nat-rule-match"/) { '"match"' }

      # Fix .xsd: "snmp system-name" should be "snmp name"
      str.gsub! '"system-name" arg', '"name" (quote | arg)'

      # Fix .xsd: argument of "system license keys key" can be quoted
      str.gsub!(/^(rule\(:license_object\) do.*?"key") arg/m) { "#{$1} (quote | arg)" }

      # Fix .xsd: "prefix-limit teardown"
      str.gsub!(/^((\s*)"maximum" arg,)\s*"teardown" (\(.*?as\(:oneline\)\s*\)\s*\))/m) do
        "#{$1}\n#{$2}\"teardown\" arg #{$3},\n#{$2}\"teardown\""
      end

      # Fix .xsd: faster interface speed support
      str.gsub! '"400g" | "oc', '"400g" | "800g" | "oc'
      str.gsub! '"400G"', '"400G" | "800G"'

      # Fix .xsd: "poe"
      str.gsub!(/^(\s*"interface" \("all" \| "all-extended" \| arg\) \(\s*)c\(/) { "#{$1}sc(" }
      str.gsub!(/^(\s*)\)(,\s*"fpc" enum\(\("all" \| "fpc-slot-number"\)\) \(\s*)c\((.*"lldp-priority"\s*\)\s*\))/m) do
        "#{$1}).as(:oneline)#{$2}sc(#{$3}.as(:oneline)"
      end

      # Fix .xsd: support "set protocols iccp peer xxx liveness-detection single-hop"
      str.gsub!(/(^rule\(:peer_group\) do.*?\n(\s*)"detection-time" \(\s*c\(\s*"threshold" arg\s*\)\s*\))/m) do
        "#{$1},\n#{fmt('"single-hop"', $2)}"
      end

      # Fix .xsd: support "set interfaces xxx ether-options speed"
      str.gsub! '"ethernet-1', '"1'

      # Fix .xsd: support "set policy-options policy-statement xxx from policy [expression]"
      str.gsub!(/^rule\(:policy_algebra\) do(\s*)arg\.as\(:arg\)\send/) do
        "rule(:policy_algebra) do#{$1}any.as(:arg)\nend"
      end

      # Fix .xsd: support "set interfaces xxx enable"
      str.gsub!(/^(rule\(:interfaces_type\) do\s*[^\n]*\s*c\()(\s*)/m) do
        %(#{$1}#{$2}"enable",#{$2})
      end

      # Fix .xsd: arg should be regular_expression
      str.gsub!(/^(\s*"(allow|deny)-(commands|configuration)-regexps") arg/) { "#{$1} regular_expression" }

      str
    end

    def fmt(str, offset = OFFSET)
      case str
      when String
        str.empty? ? '' : offset + str
      when Array
        str.map { |s| s.empty? ? '' : offset + s.to_s }.join("\n")
      else
        ''
      end
    end

    def rule_header
      <<~HEADER
        require 'parslet'

        module Junoser
          class Parser < Parslet::Parser
            def parse_lines(config)
              lines = config.split("\\n").map(&:strip)
              lines_without_deactivate = lines.reject {|l| l =~ /^deactivate/ }

              lines.inject(true) do |passed, line|
                passed & parse_line(line, lines_without_deactivate)
              end
            end

            def parse_line(line, lines_without_deactivate)
              if line =~ /^deactivate/
                if lines_without_deactivate.grep(/^\#{line.sub(/^deactivate/, 'set')}/).empty?
                  $stderr.puts %(Corresponding "set" statement is not found:  \#{line})
                  return false
                else
                  return true
                end
              end

              begin
                # .xsd doesn't include "apply-groups"
                if line =~ /(.*)\\s+apply-groups(-except)?\\s+(\\S+|\\[.*\\])$/
                  return $1 == 'set' ? true : parse($1)
                end

                parse line
                true
              rescue Parslet::ParseFailed
                $stderr.puts "Invalid syntax:  \#{line}"
                false
              end
            end

            # block with children maybe
            def b(object, *children)
              children.inject(object) {|rule, child| rule.as(:label) >> (space >> child.as(:child) | eos) }
            end

            # with an argument, and children maybe
            def a(object, arg, *children)
              b(object.as(:statement) >> space >> arg.as(:argument), *children)
            end

            # choice
            def c(*objects)
              objects.inject {|rule, object| rule | object }
            end

            def ca(*objects)
              objects.inject {|rule, object| rule | object } | arg
            end

            # sequence
            def s(*objects)
              # TODO:  eval "minOccurs" attribute of choice element
              objects.inject {|rule, object| rule >> (space >> object).maybe }
            end

            # sequential choice
            def sc(*objects)
              (c(*objects) >> space.maybe).repeat(0)
            end

            def sca(*objects)
              (c(*objects, arg) >> space.maybe).repeat(0)
            end

            def enum(object)
              (object.as(:enum))
            end

            rule(:arg)     { match('\\S').repeat(1) }
            rule(:space)   { match('\\s').repeat(1) }
            rule(:any)     { match('.').repeat(1) }
            rule(:eos)     { match('$') }
            rule(:dotted)  { match('[^. \\t\\n\\r\\f]').repeat(1) >> str('.') >> match('[^. \\t\\n\\r\\f]').repeat(1) }
            rule(:quote)   { str('"') >> match('[^"]').repeat(1) >> str('"') }
            rule(:address) { match('[0-9a-fA-F:.]').repeat(1) }
            rule(:prefix ) { address >> (str('/') >> match('[0-9]').repeat(1)).maybe }

            root(:set)
            rule(:set) { str('set') >> space >> configuration.as(:config) >> comment.maybe }

            rule(:comment) { space.maybe >> (hash_comment | slash_asterisk) }
            rule(:hash_comment) { str('#') >> any.maybe }
            rule(:slash_asterisk) { str('/*') >> match('(?!\\*\\/).').repeat(0) >> str('*/') }

      HEADER
    end

    def rule_footer
      <<~FOOTER

          end
        end
      FOOTER
    end
  end
end

# rubocop:enable Style/PerlBackrefs
