module DRYSpec
  module Helpers
    # This allows us to simplify the case where we want to
    # have a context which contains one or more `let` statements
    #
    # Supports giving either a Hash or a String and a Hash as arguments
    # In both cases the Hash will be used to define `let` statements
    # When a String is specified that becomes the context description
    # If String isn't specified, Hash#inspect becomes the context description
    #
    # @example Defining a simple let variable
    #   # Before
    #   subject { a + 1 }
    #   context('a is 1') do
    #     let(:a) { 1 }
    #     it { should eq 2 }
    #   end
    #
    #   # After
    #   subject { a + 1 }
    #   let_context(a: 1) { it { should eq 2 } }
    #
    # @example Giving a descriptive string
    #   subject { a + 1 }
    #   let_context('Negative number', a: -1) { it { should eq 0 } }
    #
    # @example Multiple variables
    #   subject { a + b }
    #   let_context(a: 1, b: 2) { it { should eq 3 } }
    def let_context(*args, &block)
      context_string, hash =
        case args.map(&:class)
        when [String, Hash] then ["#{args[0]} #{args[1]}", args[1]]
        when [Hash] then [args[0].inspect, args[0]]
        end

      context(context_string) do
        hash.each { |var, value| let(var) { value } }

        instance_eval(&block)
      end
    end

    # Allows you to simply specify that the subject should raise an exception
    # Takes no arguments or arguments of an exception class, a string, or both.
    #
    # As with RSpec's basic `to raise_error` matcher, if you don't give an error
    # then the an unexpected error may cause your test to pass incorrectly
    #
    # @example Raising a string
    #   # Before
    #   subject { fail 'Test' }
    #   it "should raise 'Test'" do
    #     expect { subject }.to raise_error 'Test'
    #   end
    #
    #   # After
    #   subject { fail 'Test' }
    #   subject_should_raise 'Test'
    #
    # @example Raising an exception class
    #   subject { fail ArgumentError }
    #   subject_should_raise ArgumentError
    #
    # @example Raising an exception class and string
    #   subject { fail ArgumentError, 'Test' }
    #   subject_should_raise ArgumentError, 'Test'
    def subject_should_raise(*args)
      error, message = args
      it_string = "subject should raise #{error}"
      it_string += " (#{message.inspect})" if message

      it it_string do
        expect { subject }.to raise_error error, message
      end
    end

    # Allows you to simply specify that the subject should not raise an exception.
    # Takes no arguments or arguments of an exception class, a string, or both.
    #
    # As with RSpec's basic `not_to raise_error` matcher, if you give a specific error, other
    # unexpected errors may be swallowed silently
    #
    # @example Subject does not raise an error
    #   # Before
    #   subject { 1 }
    #   it 'should not raise an exception' do
    #     expect { subject }.not_to raise_error
    #   end
    #
    #   # After
    #   subject { 1 }
    #   subject_should_not_raise
    def subject_should_not_raise(*args)
      error, message = args
      it_string = "subject should not raise #{error}"
      it_string += " (#{message.inspect})" if message

      it it_string do
        expect { subject }.not_to raise_error error, message
      end
    end

    # A shortcut for a combination of `context` and `subject`
    #
    # @example Multiple subjects from let variables
    #
    #   let(:foo) { 55 }
    #   let(:bar) { 66 }
    #
    #   # Before
    #   context 'subject: foo' do
    #     subject { foo }
    #
    #     it { should eq(55) }
    #   end
    #
    #   context 'subject: bar' do
    #     subject { bar }
    #
    #     it { should eq(66) }
    #   end
    #
    #   # After
    #   for_subject(:foo) { it { should eq(55) } }
    #   for_subject(:bar) { it { should eq(66) } }
    def for_subject(variable, &block)
      context "subject: #{variable}" do
        subject { send(variable) }

        instance_eval(&block)
      end
    end
  end
end


# encoding: UTF-8
#--
# Copyright (c) 2017 Translation Exchange, Inc
#
#  _______                  _       _   _             ______          _
# |__   __|                | |     | | (_)           |  ____|        | |
#    | |_ __ __ _ _ __  ___| | __ _| |_ _  ___  _ __ | |__  __  _____| |__   __ _ _ __   __ _  ___
#    | | '__/ _` | '_ \/ __| |/ _` | __| |/ _ \| '_ \|  __| \ \/ / __| '_ \ / _` | '_ \ / _` |/ _ \
#    | | | | (_| | | | \__ \ | (_| | |_| | (_) | | | | |____ >  < (__| | | | (_| | | | | (_| |  __/
#    |_|_|  \__,_|_| |_|___/_|\__,_|\__|_|\___/|_| |_|______/_/\_\___|_| |_|\__,_|_| |_|\__, |\___|
#                                                                                __/ |
#                                                                                |___/
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++

module Tml

  class << self
    attr_accessor :config
  end

  # Initializes default config

  def self.config
    @config ||= Tml::Config.new
  end

  # Allows you to configure Tml
  #
  # Tml.configure do |config|
  #    config.application = {:key => "", :secret => ""}
  #
  # end
  #

  def self.configure
    yield(self.config)
  end

  # Allows you to create a block to perform something on adjusted config settings
  # Once the block exists, the config will be reset back to what it was before:
  #
  #  Tml.with_config_settings do |config|
  #     config.format = :text
  #
  #     Do something....
  #
  #  end
  #

  def self.with_config_settings
    old_config = @config.dup
    yield(@config)
    @config = old_config
  end

  # Acts as a global singleton that holds all Tml configuration
  # The class can be extended with a different implementation, as long as the interface is supported
  class Config
    # Configuration Attributes
    attr_accessor :enabled, :locale, :default_level, :format, :application, :postoffice, :context_rules, :logger, :cache, :default_tokens, :localization
    attr_accessor :auto_init, :source_separator, :domain

    # Used by Rails and Sinatra extensions
    attr_accessor :current_locale_method, :current_user_method, :translator_options, :i18n_backend
    attr_accessor :invalidator, :agent, :api_client

    # Used for IRB only
    attr_accessor :submit_missing_keys_realtime

    def initialize
      @enabled = true
      @default_level  = 0
      @format = :html
      @auto_init = true
      @source_separator = '@:@'

      @api_client = {
        class: Tml::Api::Client,
        timeout: 5,
        open_timeout: 2
      }

      @locale = {
        default:      'en',       # default locale
        method:       'current_locale', # method to use for user selected locale, if nil, the rest will be a fallback
        param:        'locale',   # the way to name the param
        strategy:     'param',    # approach param, pre-path, pre-domain, custom-domain
        redirect:     true,       # if TML should handle locale logic redirects
        skip_default: false,      # if the default locales should not be visible
        browser:      true,       # if you want to use a browser header to determine the locale
        cookie:       true,       # if you want to store user selected locale in the cookie
        # default_host: '',         # if user wants to not display default locale, we need to know the default host
        # mapping: {                # domain to locale mapping
        #   en: '',
        #   ru: '',
        # }
      }

      @agent = {
        enabled: true,
        type:    'agent',
        cache:   86400  # timeout every 24 hours
      }

      # if running from IRB, make it default to TRUE
      @submit_missing_keys_realtime = (%w(irb pry).include?($0 || ''))

      @current_locale_method = :current_locale
      @current_user_method = :current_user

      @application = nil
      @postoffice = nil

      @translator_options = {
        debug: false,
        debug_format_html: "{ {$0} }",
        debug_format: '{{{{$0}}}}',
        split_sentences: false,
        nodes: {
          ignored:    %w(),
          scripts:    %w(style script code pre),
          inline:     %w(a span i b img strong s em u sub sup),
          short:      %w(i b),
          splitters:  %w(br hr)
        },
        attributes: {
          labels:     %w(title alt)
        },
        name_mapping: {
          b:    'bold',
          i:    'italic',
          a:    'link',
          img:  'picture'
        },
        data_tokens: {
          special: {
            enabled: true,
            regex: /(&[^;]*;)/
          },
          date: {
            enabled: false,
            formats: [
              [/((Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d+,\s+\d+)/, "{month} {day}, {year}"],
              [/((January|February|March|April|May|June|July|August|September|October|November|December)\s+\d+,\s+\d+)/, "{month} {day}, {year}"],
              [/(\d+\s+(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec),\s+\d+)/, "{day} {month}, {year}"],
              [/(\d+\s+(January|February|March|April|May|June|July|August|September|October|November|December),\s+\d+)/, "{day} {month}, {year}"]
            ],
            name: 'date'
          },
          rules: [
            {enabled: false, name: 'time',     regex: /(\d{1,2}:\d{1,2}\s+([A-Z]{2,3}|am|pm|AM|PM)?)/},
            {enabled: false, name: 'phone',    regex: /((\d{1}-)?\d{3}-\d{3}-\d{4}|\d?\(\d{3}\)\s*\d{3}-\d{4}|(\d.)?\d{3}.\d{3}.\d{4})/},
            {enabled: false, name: 'email',    regex: /([-a-z0-9~!$%^&*_=+}{\'?]+(\.[-a-z0-9~!$%^&*_=+}{\'?]+)*@([a-z0-9_][-a-z0-9_]*(\.[-a-z0-9_]+)*\.(aero|arpa|biz|com|coop|edu|gov|info|int|mil|museum|name|net|org|pro|travel|io|mobi|[a-z][a-z])|([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}))(:[0-9]{1,5})?)/},
            {enabled: false, name: 'price',    regex: /(\$\d*(,\d*)*(\.\d*)?)/},
            {enabled: false, name: 'fraction', regex: /(\d+\/\d+)/},
            {enabled: false, name: 'num',      regex: /(\b\d*(,\d*)*(\.\d*)?%?\b)/}
          ]
        }
      }

      @context_rules = {
        :number => {
          :variables => {
          }
        },
        :gender => {
          :variables => {
              '@gender' => 'gender',
          }
        },
        :genders => {
          :variables => {
            '@genders' => lambda{|list| list.collect do |u|
                u.is_a?(Hash) ? (u['gender'] || u[:gender]) : u.gender
            end
            },
            '@size' => lambda{ |list| list.size }
          }
        },
        :date => {
          :variables => {
          }
        },
        :time => {
          :variables => {
          }
        },
        :list => {
          :variables => {
            '@count' => lambda{|list| list.size}
          }
        },
      }

      @cache = nil

      @logger  = {
        :enabled        => false,
        :path           => './log/tml.log',
        :level          => 'debug',
        :secure         => true
      }

      @default_tokens = {
        :html => {
          :data => {
            :ndash  =>  '–',       # –
            :mdash  =>  '—',       # —
            :iexcl  =>  '¡',       # ¡
            :iquest =>  '¿',      # ¿
            :quot   =>  '"',        # '
            :ldquo  =>  '“',       # “
            :rdquo  =>  '”',       # ”
            :lsquo  =>  '‘',       # ‘
            :rsquo  =>  '’',       # ’
            :laquo  =>  '«',       # «
            :raquo  =>  '»',       # »
            :nbsp   =>  ' ',        # space
            :lsaquo =>  '‹',      # ‹
            :rsaquo =>  '›',      # ›
            :br     =>  '
',         # line break
            :lbrace =>  '{',
            :rbrace =>  '}',
            :trade  =>  '™',       # TM
          },
          :decoration => {
            :anchor =>  "{$0}",
            :strong =>  '{$0}',
            :bold   =>  '{$0}',
            :b      =>  '{$0}',
            :em     =>  '{$0}',
            :italic =>  '{$0}',
            :i      =>  '{$0}',
            :link   =>  "{$0}",
            :br     =>  '
{$0}',
            :strike =>  '{$0}',
            :div    =>  "
{$0}
",
            :span   =>  "{$0}",
            :h1     =>  '
{$0}
',
            :h2     =>  '
{$0}
',
            :h3     =>  '
{$0}
',
          }
        },
        :text => {
          :data => {
            :ndash  =>  '–',
            :mdash  =>  '-',
            :iexcl  =>  '¡',
            :iquest =>  '¿',
            :quot   =>  "'",
            :ldquo  =>  '“',
            :rdquo  =>  '”',
            :lsquo  =>  '‘',
            :rsquo  =>  '’',
            :laquo  =>  '«',
            :raquo  =>  '»',
            :nbsp   =>  ' ',
            :lsaquo =>  '‹',
            :rsaquo =>  '›',
            :br     =>  '\n',
            :lbrace =>  '{',
            :rbrace =>  '}',
            :trade  =>  '™',
          },
          :decoration => {
            :anchor =>  '{$0}',
            :strong =>  '{$0}',
            :bold   =>  '{$0}',
            :b      =>  '{$0}',
            :em     =>  '{$0}',
            :italic =>  '{$0}',
            :i      =>  '{$0}',
            :link   =>  '{$0}:{$1}',
            :br     =>  '\n{$0}',
            :strike =>  '{$0}',
            :div    =>  '{$0}',
            :span   =>  '{$0}',
            :h1     =>  '{$0}',
            :h2     =>  '{$0}',
            :h3     =>  '{$0}',
          }
        }
      }

      @localization = {
        :default_day_names        => %w(Sunday Monday Tuesday Wednesday Thursday Friday Saturday),
        :default_abbr_day_names   => %w(Sun Mon Tue Wed Thu Fri Sat),
        :default_month_names      => %w(January February March April May June July August September October November December),
        :default_abbr_month_names => %w(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec),
        :custom_date_formats      =>  {
          :default                => '%m/%d/%Y',            # 07/4/2008
          :short_numeric          => '%m/%d',               # 07/4
          :short_numeric_year     => '%m/%d/%y',            # 07/4/08
          :long_numeric           => '%m/%d/%Y',            # 07/4/2008
          :verbose                => '%A, %B %d, %Y',       # Friday, July  4, 2008
          :monthname              => '%B %d',               # July 4
          :monthname_year         => '%B %d, %Y',           # July 4, 2008
          :monthname_abbr         => '%b %d',               # Jul 4
          :monthname_abbr_year    => '%b %d, %Y',           # Jul 4, 2008
          :date_time              => '%m/%d/%Y at %H:%M',   # 01/03/1010 at 5:30
        },
        :token_mapping => {
          '%a' => '{short_week_day_name}',
          '%A' => '{week_day_name}',
          '%b' => '{short_month_name}',
          '%B' => '{month_name}',
          '%p' => '{am_pm}',
          '%d' => '{days}',
          '%e' => '{day_of_month}',
          '%j' => '{year_days}',
          '%m' => '{months}',
          '%W' => '{week_num}',
          '%w' => '{week_days}',
          '%y' => '{short_years}',
          '%Y' => '{years}',
          '%l' => '{trimed_hour}',
          '%H' => '{full_hours}',
          '%I' => '{short_hours}',
          '%M' => '{minutes}',
          '%S' => '{seconds}',
          '%s' => '{since_epoch}'
        }
      }
    end

    def enabled?
      enabled
    end

    def disabled?
      not enabled?
    end

    def locale_expression
      /^[a-z]{2}(-[A-Z]{2,3})?$/
    end

    def locale_strategy
      locale[:strategy] || 'param'
    end

    def agent_locale_strategy
      locale.merge(param: locale_param)
    end

    def locale_param
      locale[:param] || 'locale'
    end

    def current_locale_method
      locale[:method] || @current_locale_method
    end

    def locale_cookie_enabled?
      if %w(pre-domain custom-domain).include?(locale[:strategy])
        return false
      end
      locale[:cookie].nil? || locale[:cookie]
    end

    def locale_browser_enabled?
      locale[:browser].nil? || locale[:browser]
    end

    def locale_redirect_enabled?
      locale[:redirect].nil? || locale[:redirect]
    end

    def nested_value(hash, key, default_value = nil)
      parts = key.split('.')
      parts.each do |part|
        return default_value unless hash[part.to_sym]
        hash = hash[part.to_sym]
      end
      hash
    end

    def translator_option(key)
      nested_value(self.translator_options, key)
    end

    def cache_enabled?
       not cache.nil?
    end

    #########################################################
    ## Application
    #########################################################

    #
    #def default_level
    #  return Tml.session.application.default_level if Tml.session.application
    #  @default_level
    #end

    def xmessage_rule_key_mapping
      @rule_key_mapping ||= {
          number: {
              one: 'singular',
              few: 'few',
              many: 'many',
              other: 'plural'
          },
          gender: {
              male: 'male',
              female: 'female',
              neutral: 'neutral',
              other: 'other',
          },
          date: {
              future: 'future',
              present: 'present',
              past: 'past'
          }
      }
    end

    def xmessage_decoration_tokens
      @xmessage_tokens ||= @default_tokens[:html][:decoration].keys
    end

    def default_locale
      @locale[:default] || 'en'
    end

    def default_language
      @default_language ||= begin
        file = File.expand_path(File.join(File.dirname(__FILE__), '..', 'tml', 'languages', "#{Tml.config.default_locale}.json"))
        Tml::Language.new(JSON.parse(File.read(file)))
      end
    end

    def default_application
      @default_application ||= Tml::Application.new(:host => Tml::Api::Client::API_HOST)
    end

    def access_token
      @application[:token] || @application[:access_token] || ''
    end

    #########################################################
    ## Decorations
    #########################################################

    def decorator_class
      return Tml::Decorators::Html if @format == :html
      Tml::Decorators::Default
    end

    def default_token_value(token_name, type = :data, format = :html)
      default_tokens[format.to_sym][type.to_sym][token_name.to_sym]
    end

    def set_default_token(token_name, value, type = :data, format = :html)
      default_tokens[format.to_sym] ||= {}
      default_tokens[format.to_sym][type.to_sym] ||= {}
      default_tokens[format.to_sym][type.to_sym][token_name.to_sym] = value
    end

    #########################################################
    ## Localization
    #########################################################

    def strftime_symbol_to_token(symbol)
      localization[:token_mapping][symbol]
    end

    def default_day_names
      localization[:default_day_names]
    end

    def default_day_name(index)
      '' + default_day_names[index]
    end

    def default_abbr_day_names
      localization[:default_abbr_day_names]
    end

    def default_abbr_day_name(index)
      '' + default_abbr_day_names[index]
    end

    def default_month_names
      localization[:default_month_names]
    end

    def default_month_name(index)
      '' + default_month_names[index]
    end

    def default_abbr_month_names
      localization[:default_abbr_month_names]
    end

    def default_abbr_month_name(index)
      '' + default_abbr_month_names[index]
    end

    def default_date_formats
      localization[:custom_date_formats]
    end

  end
end


module Janus
  class Manager
    include Janus::Hooks
    include Janus::Strategies

    attr_reader :request, :cookies

    def initialize(request, cookies)
      @request, @cookies = request, cookies
    end

    # Tries to authenticate the user using strategies, before returning
    # the current user or nil.
    def authenticate(scope)
      run_strategies(scope) unless authenticated?(scope)
      user(scope)
    end

    # Raises a Janus::NotAuthenticated exception unless a user is authenticated.
    def authenticate!(scope)
      raise Janus::NotAuthenticated.new(scope) unless authenticate?(scope)
    end

    # Tries to authenticate the user before checking if it's authenticated.
    def authenticate?(scope)
      authenticate(scope)
      authenticated?(scope)
    end

    # Returns true if a user is authenticated.
    def authenticated?(scope) # :nodoc:
      !!session(scope)
    end

    # Logs a user in.
    #
    # FIXME: what should happen when a user signs in but a user is already signed in for the same scope?!
    def login(user, options = {})
      options[:scope] ||= Janus.scope_for(user)
      set_user(user, options)
      Janus::Manager.run_callbacks(:login, user, self, options)
    end

    # Logs a user out from the given scopes or from all scopes at once
    # if no scope is defined. If no scope is left after logout, then the
    # whole session will be resetted.
    def logout(*scopes)
      scopes = janus_sessions.keys if scopes.empty?

      scopes.each do |scope|
        _user = user(scope)
        unset_user(scope)
        Janus::Manager.run_callbacks(:logout, _user, self, :scope => scope)
      end

      request.reset_session if janus_sessions.empty?
    end

    # Manually sets a user without going throught the whole login or
    # authenticate process.
    def set_user(user, options = {})
      scope = options[:scope] || Janus.scope_for(user)
      janus_sessions[scope.to_s] = { 'user_class' => user.class.name, 'user_id' => user.id }
    end

    # Manually removes the user without going throught the whole logout process.
    def unset_user(scope)
      janus_sessions.delete(scope.to_s)
      @users.delete(scope.to_sym) unless @users.nil?
    end

    # Returns the currently connected user.
    def user(scope)
      scope = scope.to_sym
      @users ||= {}

      if authenticated?(scope)
        if @users[scope].nil?
          begin
          @users[scope] = user_class(scope).find(session(scope)['user_id'])
          rescue ActiveRecord::RecordNotFound
            unset_user(scope)
          else
            Janus::Manager.run_callbacks(:fetch, @users[scope], self, :scope => scope)
          end
        end

        @users[scope]
      end
    end

    # Returns the current session for user.
    def session(scope)
      janus_sessions[scope.to_s]
    end

    private
      def janus_sessions
        request.session['janus'] ||= {}
      end

      def user_class(scope)
        session(scope)['user_class'].constantize
      end
  end
end


# encoding: UTF-8
#--
# Copyright (c) 2017 Translation Exchange, Inc
#
#  _______                  _       _   _             ______          _
# |__   __|                | |     | | (_)           |  ____|        | |
#    | |_ __ __ _ _ __  ___| | __ _| |_ _  ___  _ __ | |__  __  _____| |__   __ _ _ __   __ _  ___
#    | | '__/ _` | '_ \/ __| |/ _` | __| |/ _ \| '_ \|  __| \ \/ / __| '_ \ / _` | '_ \ / _` |/ _ \
#    | | | | (_| | | | \__ \ | (_| | |_| | (_) | | | | |____ >  < (__| | | | (_| | | | | (_| |  __/
#    |_|_|  \__,_|_| |_|___/_|\__,_|\__|_|\___/|_| |_|______/_/\_\___|_| |_|\__,_|_| |_|\__, |\___|
#                                                                                __/ |
#                                                                                |___/
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++

require 'rubygems'
require 'rubygems/package'
require 'open-uri'
require 'zlib'
require 'fileutils'

module Tml

  def self.cache
    @cache ||= begin
      if Tml.config.cache_enabled?
        # .capitalize is not ideal, but .camelcase is not available in pure ruby.
        # This works for the current class names.
        klass = Tml::CacheAdapters.const_get(Tml.config.cache[:adapter].to_s.capitalize)
        klass.new
      else
        # blank implementation
        Tml::Cache.new
      end
    end
  end

  class Cache

    # version object
    def version
      @version ||= Tml::CacheVersion.new(self)
    end

    # resets current version
    def reset_version
      version.reset
    end

    # upgrade current version
    def upgrade_version
      version.upgrade
    end

    # checks if Tml is enabled
    def enabled?
      Tml.config.cache_enabled?
    end

    # by default all cache is read/write
    # cache like files based should be set to read only
    def read_only?
      false
    end

    # name of the cache adapter
    def cache_name
      self.class.name.split('::').last
    end

    # logs information messages
    def info(msg)
      Tml.logger.info("#{cache_name} - #{msg}")
    end

    # logs a warning
    def warn(msg)
      Tml.logger.warn("#{cache_name} - #{msg}")
    end

    def namespace=(value)
      @namespace = value
    end

    # namespace of each cache key
    def namespace
      return '#' if Tml.config.disabled?
      @namespace || Tml.config.cache[:namespace] || Tml.config.application[:key][0..5]
    end

    # versioned name of cache key
    def versioned_key(key, opts = {})
      version.versioned_key(key, namespace)
    end

    # fetches key from cache
    def fetch(key, opts = {})
      return nil unless block_given?
      yield
    end

    # stores key in cache
    def store(key, data, opts = {})
      # do nothing
    end

    # deletes key from cache
    def delete(key, opts = {})
      # do nothing
    end

    # checks if the key exists
    def exist?(key, opts = {})
      false
    end

    # clears cache
    def clear(opts = {})
      # do nothing
    end

    # Pulls cache version from CDN
    def extract_version(app, version = nil)
      if version
        Tml.cache.version.set(version.to_s)
      else
        version_data = app.api_client.get_from_cdn('version', {t: Time.now.to_i}, {uncompressed: true})

        unless version_data
          Tml.logger.debug('No releases have been generated yet. Please visit your Dashboard and publish translations.')
          return
        end

        Tml.cache.version.set(version_data['version'])
      end
    end

    # Warms up cache from CDN or local files
    def warmup(version = nil, cache_path = nil)
      if cache_path.nil?
        warmup_from_cdn(version)
      else
        warmup_from_files(version, cache_path)
      end
    end

    # Warms up cache from local files
    def warmup_from_files(version = nil, cache_path = nil)
      t0 = Time.now
      Tml.logger = Logger.new(STDOUT)

      Tml.logger.debug('Starting cache warmup from local files...')
      version ||= Tml.config.cache[:version]
      cache_path ||= Tml.config.cache[:path]
      cache_path = "#{cache_path}/#{version}"

      Tml.cache.version.set(version.to_s)
      Tml.logger.debug("Warming Up Version: #{Tml.cache.version}")

      application = JSON.parse(File.read("#{cache_path}/application.json"))
      Tml.cache.store(Tml::Application.cache_key, application)

      sources = JSON.parse(File.read("#{cache_path}/sources.json"))

      application['languages'].each do |lang|
        locale = lang['locale']

        language = JSON.parse(File.read("#{cache_path}/#{locale}/language.json"))
        Tml.cache.store(Tml::Language.cache_key(locale), language)

        sources.each do |src|
          source = JSON.parse(File.read("#{cache_path}/#{locale}/sources/#{src}.json"))
          Tml.cache.store(Tml::Source.cache_key(locale, src), source)
        end
      end

      t1 = Time.now
      Tml.logger.debug("Cache warmup took #{t1-t0}s")
    end

    # Warms up cache from CDN
    def warmup_from_cdn(version = nil)
      t0 = Time.now
      Tml.logger = Logger.new(STDOUT)

      Tml.logger.debug('Starting cache warmup from CDN...')
      app = Tml::Application.new(key: Tml.config.application[:key], cdn_host: Tml.config.application[:cdn_host])
      extract_version(app, version)

      Tml.logger.debug("Warming Up Version: #{Tml.cache.version}")

      application = app.api_client.get_from_cdn('application', {t: Time.now.to_i})
      Tml.cache.store(Tml::Application.cache_key, application)

      sources = app.api_client.get_from_cdn('sources', {t: Time.now.to_i}, {uncompressed: true})

      application['languages'].each do |lang|
        locale = lang['locale']
        language = app.api_client.get_from_cdn("#{locale}/language", {t: Time.now.to_i})
        Tml.cache.store(Tml::Language.cache_key(locale), language)

        sources.each do |src|
          source = app.api_client.get_from_cdn("#{locale}/sources/#{src}", {t: Time.now.to_i})
          Tml.cache.store(Tml::Source.cache_key(locale, src), source)
        end
      end

      t1 = Time.now
      Tml.logger.debug("Cache warmup took #{t1-t0}s")
    end

    # default cache path
    def default_cache_path
      @cache_path ||= begin
        path = Tml.config.cache[:path]
        path ||= 'config/tml'
        FileUtils.mkdir_p(path)
        FileUtils.chmod(0777, path)
        path
      end
    end

    # downloads cache from the CDN
    def download(cache_path = default_cache_path, version = nil)
      t0 = Time.now
      Tml.logger = Logger.new(STDOUT)

      Tml.logger.debug('Starting cache download...')
      app = Tml::Application.new(key: Tml.config.application[:key], cdn_host: Tml.config.application[:cdn_host])
      extract_version(app, version)

      Tml.logger.debug("Downloading Version: #{Tml.cache.version}")

      archive_name = "#{Tml.cache.version}.tar.gz"
      path = "#{cache_path}/#{archive_name}"
      url = "#{app.cdn_host}/#{Tml.config.application[:key]}/#{archive_name}"

      Tml.logger.debug("Downloading cache file: #{url}")
      open(path, 'wb') do |file|
        file << open(url).read
      end

      Tml.logger.debug('Extracting cache file...')
      version_path = "#{cache_path}/#{Tml.cache.version}"
      Tml::Utils.untar(Tml::Utils.ungzip(File.new(path)), version_path)
      Tml.logger.debug("Cache has been stored in #{version_path}")

      File.unlink(path)

      begin
        current_path = 'current'
        FileUtils.chdir(cache_path)
        FileUtils.rm(current_path) if File.exist?(current_path)
        FileUtils.ln_s(Tml.cache.version.to_s, current_path)
        Tml.logger.debug("The new version #{Tml.cache.version} has been marked as current")
      rescue Exception => ex
        Tml.logger.debug("Could not generate current symlink to the cache path: #{ex.message}")
      end

      t1 = Time.now
      Tml.logger.debug("Cache download took #{t1-t0}s")
    end

    # remove extensions
    def strip_extensions(data)
      if data.is_a?(Hash)
        data = data.dup
        data.delete('extensions')
        return data
      end

      if data.is_a?(String) and data.match(/^\{/)
        data = JSON.parse(data)
        data.delete('extensions')
        data = data.to_json
      end

      data
    end

  end
end


require 'bdb'
require 'sbdb/cursor'

module SBDB
	TYPES = []
	class DB
		UNKNOWN = Bdb::Db::UNKNOWN
		BTREE = Bdb::Db::BTREE
		HASH = Bdb::Db::HASH
		QUEUE = Bdb::Db::QUEUE
		ARRAY = RECNO = Bdb::Db::RECNO
		RDONLY = READLONY = Bdb::DB_RDONLY
		CONSUME = Bdb::DB_CONSUME
		CONSUME_WAIT = Bdb::DB_CONSUME_WAIT

		attr_reader :home
		attr_accessor :txn
		include Enumerable
		def bdb_object()  @db  end
		def sync()  @db.sync  end
		def close( flg = nil)  @db.close flg || 0  end
		def cursor( &exe)  Cursor.new self, &exe  end

		def at key, txn = nil
			@db.get _txn(txn), key.nil? ? nil : key.to_s, nil, 0
		rescue Bdb::KeyEmpty
			return nil
		end
		alias [] at

		def put key, val, txn = nil
			if val.nil?
				@db.del _txn(txn), key.to_s, 0
			else
				@db.put _txn(txn), key.nil? ? nil : key.to_s, val.to_s, 0
			end
		end

		def []= key, val
			put key, val
		end

		def delete key, txn = nil
			@db.del _txn(txn), key.to_s
		end
		alias del delete

		class << self
			def new *ps, &exe
				ret = obj = super( *ps)
				begin ret = exe.call obj
				ensure
					SBDB::raise_barrier &obj.method(:sync)
					SBDB::raise_barrier &obj.method(:close)
				end  if exe
				ret
			end
			alias open new
		end

		def _txn txn
			txn ||= @txn
			txn && txn.bdb_object
		end

		def transaction flg = nil, &exe
			block_given? ? home.transaction( flg, &exe) : home.transaction( flg)
		end

		# Arguments:
		# * file
		# * name
		# * type
		# * flags
		# * mode
		# * env
		# or: **file**, **opts**. *opts* must be a *::Hash* with keys like above, excluded *file*.
		def initialize file, *args
			opts = ::Hash === args.last ? args.pop : {}
			opts = {:name => args[0], :type => args[1], :flags => args[2], :mode => args[3], :env => args[4]}.update opts
			#type = BTREE  if type == UNKNOWN and (flags & CREATE) == CREATE
			@home, @db = opts[:env], opts[:env] ? opts[:env].bdb_object.db : Bdb::Db.new
			opts[:type] = TYPES.index(self.class) || UNKNOWN
			@db.re_len = opts[:re_len]  if opts[:re_len]
			@txn = nil
			txn = opts[:txn]  # First is the global txn, second only for open.
			begin
				@db.open txn && txn.bdb_object, file, opts[:name], opts[:type], opts[:flags] || 0, opts[:mode] || 0
			rescue Object => exc
				close
				raise exc
			end
		end

		def each key = nil, val = nil, &exe
			cursor {|c| c.each key, val, &exe }
		end

		def reverse key = nil, val = nil, &exe
			cursor {|c| c.reverse key, val, &exe }
		end

		def to_hash key = nil, val = nil
			ht = {}
			each key, val, &ht.method( :[]=)
			ht
		end

		def truncate txn = nil
			@db.truncate _txn(txn)
		end
	end

	class Unknown < DB
		def self.new file, *ps, &exe
			dbt = super( file, *ps) {|db| db.bdb_object.get_type }
			TYPES[dbt] ? TYPES[dbt].new( file, *ps, &exe) : super( file, *ps, &exe)
		end
	end

	class Btree < DB
	end
	TYPES[DB::BTREE] = Btree

	class Hash < DB
	end
	TYPES[DB::HASH] = Hash

	module Arrayisch
		def [] key
			super [key].pack('I')
		end

		def []= key, val
			super [key].pack('I'), val
		end

		def push val, txn = nil
			@db.put _txn(txn), "\0\0\0\0", val, Bdb::DB_APPEND
		end
	end

	class Recno < DB
		include Arrayisch
	end
	Array = Recno
	TYPES[DB::RECNO] = Recno

	class Queue < DB
		include Arrayisch
		def unshift txn = nil
			@db.get _txn(txn), "\0\0\0\0", nil, Bdb::DB_CONSUME
		end
	end
	TYPES[DB::QUEUE] = Queue
end


# encoding: UTF-8
#--
# Copyright (c) 2017           Translation Exchange, Inc
#
#  _______                  _       _   _             ______          _
# |__   __|                | |     | | (_)           |  ____|        | |
#    | |_ __ __ _ _ __  ___| | __ _| |_ _  ___  _ __ | |__  __  _____| |__   __ _ _ __   __ _  ___
#    | | '__/ _` | '_ \/ __| |/ _` | __| |/ _ \| '_ \|  __| \ \/ / __| '_ \ / _` | '_ \ / _` |/ _ \
#    | | | | (_| | | | \__ \ | (_| | |_| | (_) | | | | |____ >  < (__| | | | (_| | | | | (_| |  __/
#    |_|_|  \__,_|_| |_|___/_|\__,_|\__|_|\___/|_| |_|______/_/\_\___|_| |_|\__,_|_| |_|\__, |\___|
#                                                                                __/ |
#                                                                                |___/
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++

module Tml

  def self.session
    Thread.current[:tml_context] ||= Tml::Session.new
  end

  class Session
    attr_accessor :application, :block_options
    attr_accessor :current_user, :current_locale, :current_language, :current_translator, :current_source

    def init(opts = {})
      return if Tml.config.disabled?

      # Tml.logger.debug(opts.inspect)

      Tml.cache.reset_version
      Tml.cache.namespace = opts[:namespace]

      init_application(opts)

      self
    end

    def init_application(opts = {})
      self.current_user = opts[:user]
      self.current_source = opts[:source] || 'index'
      self.current_locale = opts[:locale]
      self.current_translator = opts[:translator]

      app_config = Tml.config.application || {}
      self.application = Tml::Application.new(
          :key => opts[:key] || app_config[:key],
          :access_token => opts[:access_token] || opts[:token] || app_config[:token],
          :host => opts[:host] || app_config[:host],
          :cdn_host => opts[:cdn_host] || app_config[:cdn_host]
      ).fetch

      if self.current_translator
        self.current_translator.application = self.application
      end

      self.current_locale = preferred_locale(opts[:locale])
      self.current_language = self.application.current_language(self.current_locale)
    end

    def preferred_locale(locales)
      return application.default_locale unless locales
      locales = locales.is_a?(String) ? locales.split(',') : locales

      locales.each do |locale|
        locale = Tml::Language.normalize_locale(locale)
        return locale if application.locales.include?(locale)
        locale = locale.split('-').first
        return locale if application.locales.include?(locale)
      end

      application.default_locale
    end

    def reset
      self.application= nil
      self.current_user= nil
      self.current_language= nil
      self.current_translator= nil
      self.current_source= nil
      self.block_options= nil
    end

    def current_language
      @current_language ||= Tml.config.default_language
    end

    def application
      @application ||= Tml::Application.new
    end

    def source_language
      locale = block_option(:locale)
      locale ? application.language(locale) : application.language
    end

    def target_language
      target_locale = block_option(:target_locale)
      language = (target_locale ? application.language(target_locale) : current_language)
      language || Tml.config.default_language
    end

    def inline_mode?
      current_translator and current_translator.inline?
    end

    def translate(label, description = '', tokens = {}, options = {})
      params = Tml::Utils.normalize_tr_params(label, description, tokens, options)
      return params[:label] if params[:label].tml_translated?

      params[:options][:caller] ||= caller(1, 1)

      if Tml.config.disabled?
        return Tml.config.default_language.translate(params[:label], params[:tokens], params[:options]).tml_translated
      end

      # Translate individual sentences
      if params[:options][:split]
        text = params[:label]
        sentences = Tml::Utils.split_sentences(text)
        sentences.each do |sentence|
          text = text.gsub(sentence, target_language.translate(sentence, params[:description], params[:tokens], params[:options]))
        end
        return text.tml_translated
      end

      target_language.translate(params).tml_translated
    rescue Tml::Exception => ex
      #pp ex, ex.backtrace
      Tml.logger.error(ex.message)
      #Tml.logger.error(ex.message + "\n=> " + ex.backtrace.join("\n=> "))
      label
    end

    #########################################################
    ## Block Options
    #########################################################

    def block_option(key, lookup = true)
      if lookup
        block_options_queue.reverse.each do |options|
          value = options[key.to_s] || options[key.to_sym]
          return value if value
        end
        return nil
      end
      block_options[key]
    end

    def push_block_options(opts)
      block_options_queue.push(opts)
    end

    def pop_block_options
      return unless @block_options
      @block_options.pop
    end

    def block_options_queue
      @block_options ||= []
    end

    def block_options
      block_options_queue.last || {}
    end

    def with_block_options(opts)
      push_block_options(opts)
      if block_given?
        ret = yield
      end
      pop_block_options
      ret
    end
    alias_method :with_options, :with_block_options

  end
end


require 'json'
require 'thor/shell/basic'

module CouchWatcher

  class DatabaseListener

    # shortcut to say
    def say(message, color=nil)
      @shell ||= Thor::Shell::Basic.new
      @shell.say message, color
    end

    def initialize(database_url)
      @database_url = database_url
      @last_sequence = nil
      @thread = nil
      @DatabaseCallback = Struct.new(:callback_proc, :include_document)
      @database_callbacks = []
      @DocumentCallback = Struct.new(:document_id, :callback_proc, :include_document)
      @document_callbacks = []
    end

    def add_database_callback(callback_proc, include_document)
      if !@thread.nil?
        say "Error: cannot add callbacks to a running listener", :red
        return nil
      end

      database_callback = @DatabaseCallback.new(callback_proc, include_document)
      @database_callbacks << database_callback
      return database_callback
    end

    def remove_database_callback(database_callback)
      if !@thread.nil?
        say "Error: cannot remove callbacks from a running listener", :red
        return
      end

      @database_callbacks.delete(database_callback)
    end

    def add_document_callback(document_id, callback_proc, include_document)
      if !@thread.nil?
        say "Error: cannot add callbacks to a running listener", :red
        return nil
      end

      document_callback = @DocumentCallback.new(document_id, callback_proc, include_document)
      @document_callbacks << document_callback
      return document_callback
    end

    def remove_document_callback(document_callback)
      if !@thread.nil?
        say "Error: cannot remove callbacks from a running listener", :red
        return
      end

      @document_callbacks.delete(document_callback)
    end

    def start_watching(verbose, polling_interval)
      if !@thread.nil?
        say "Error: listener is already running listener", :red
        return
      end

      say "Database watching started for: #{@database_url}", :green if verbose
      @thread = Thread.new do

        # get the sequence
        if @last_sequence.nil?
          data = get_info()
          if data.nil? || data["update_seq"].nil?
            say "Error: the database cannnot be watched: #{@database_url}. Check that the couchdb is running, the database exists, and you have adequate permissions.", :red
            return
          end

          @last_sequence = data["update_seq"]
        end

        @connection_stopped = false
        while true

          #puts "Checking heartbeat: #{@database_url} sequence: #{@last_sequence} connecte"

          # stopped the connection
          if @connection_stopped
            return

          # the sequence number has been returned so start polling
          elsif @last_sequence
            results = get_changes()

            #puts "Checking heartbeat: #{@database_url} sequence: #{@last_sequence}"

            results.each do |result|

              document_id = result["id"]
              document_rev = result["changes"][0]["rev"]
              document = result["doc"]

              # tell a change
              say "Change detected in database: #{@database_url} document: #{document_id}", :green if verbose

              # call the database watchers
              @database_callbacks.each do |callback|
                if callback.include_document
                  callback.callback_proc.call(@database_url, document_id, document_rev, document)
                else
                  callback.callback_proc.call(@database_url, document_id, document_rev)
                end
              end

              # call the document watchers
              @document_callbacks.each do |callback|
                if (callback.document_id==document_id)
                  if callback.include_document
                    callback.callback_proc.call(@database_url, document_id, document_rev, document)
                  else
                    callback.callback_proc.call(@database_url, document_id, document_rev)
                  end
                end
              end
            end
          end

          sleep polling_interval
        end
      end
    end

    def stop_watching(verbose)
      if @thread.nil?
        say "Error: cannot stop because listener is is not running", :red
        return
      end

      # give the thread time to finish
      @connection_stopped = true
      @thread.kill
      @thread = nil
      say "Database watching stopped for: #{@database_url}", :green if verbose
    end

    private

    def get_info
#      puts "GET #{@database_url + "/"}"
      response = Typhoeus::Request.get(@database_url + "/")
#      puts "Response: #{response.code} #{JSON.parse(response.body).inspect}"
      json = response.code == 200 ? JSON.parse(response.body) : nil
    end

    def get_changes
#      puts "GET #{@database_url + "/_changes?since=#{@last_sequence}"}"
      response = Typhoeus::Request.get(@database_url + "/_changes?since=#{@last_sequence}&include_docs=true")
#      puts "Response: #{response.code} #{JSON.parse(response.body).inspect}"
      json = response.code == 200 ? JSON.parse(response.body) : nil
      if !json.nil?
        @last_sequence = json["last_seq"]
        return json["results"]
      end

    end

  end
end


# encoding: UTF-8
#--
# Copyright (c) 2017 Translation Exchange, Inc
#
#  _______                  _       _   _             ______          _
# |__   __|                | |     | | (_)           |  ____|        | |
#    | |_ __ __ _ _ __  ___| | __ _| |_ _  ___  _ __ | |__  __  _____| |__   __ _ _ __   __ _  ___
#    | | '__/ _` | '_ \/ __| |/ _` | __| |/ _ \| '_ \|  __| \ \/ / __| '_ \ / _` | '_ \ / _` |/ _ \
#    | | | | (_| | | | \__ \ | (_| | |_| | (_) | | | | |____ >  < (__| | | | (_| | | | | (_| |  __/
#    |_|_|  \__,_|_| |_|___/_|\__,_|\__|_|\___/|_| |_|______/_/\_\___|_| |_|\__,_|_| |_|\__, |\___|
#                                                                                __/ |
#                                                                                |___/
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++

module Tml

  CACHE_VERSION_KEY = 'current_version'

  class CacheVersion

    attr_accessor :version, :cache

    # Init cache version with cache adapter
    def initialize(cache)
      self.cache = cache
    end

    # reset cache version
    def reset
      self.version = nil
    end

    # updates the current cache version
    def set(new_version)
      self.version = new_version
    end

    # upgrade current version
    def upgrade
      cache.store(CACHE_VERSION_KEY, {'version' => 'undefined', 't' => cache_timestamp})
      reset
    end

    # validate that current cache version hasn't expired
    def validate_cache_version(version)
      # if cache version is hardcoded, use it
      if Tml.config.cache[:version]
        return Tml.config.cache[:version]
      end

      return version unless version.is_a?(Hash)
      return 'undefined' unless version['t'].is_a?(Numeric)
      return version['version'] if cache.read_only?

      # if version check interval is disabled, don't try to check for the new
      # cache version on the CDN
      if version_check_interval == -1
        Tml.logger.debug('Cache version check is disabled')
        return version['version']
      end

      expires_at = version['t'] + version_check_interval
      if expires_at < Time.now.to_i
        Tml.logger.debug('Cache version is outdated, needs refresh')
        return 'undefined'
      end

      delta = expires_at - Time.now.to_i
      Tml.logger.debug("Cache version is up to date, expires in #{delta}s")
      version['version']
    end

    # fetches the version from the cache
    def fetch
      self.version = begin
        ver = cache.fetch(CACHE_VERSION_KEY) do
          {'version' => Tml.config.cache[:version] || 'undefined', 't' => cache_timestamp}
        end
        validate_cache_version(ver)
      end
    end

    # how often should the cache be checked for
    def version_check_interval
      Tml.config.cache[:version_check_interval] || 3600
    end

    # generates cache timestamp based on an interval
    def cache_timestamp
      Tml::Utils.interval_timestamp(version_check_interval)
    end

    # stores the current version back in cache
    def store(new_version)
      self.version = new_version
      cache.store(CACHE_VERSION_KEY, {'version' => new_version, 't' => cache_timestamp})
    end

    # checks if the version is undefined
    def undefined?
      version.nil? or version == 'undefined'
    end

    # checks if version is defined
    def defined?
      not undefined?
    end

    # checks if the version is valid
    def valid?
      not invalid?
    end

    # checks if the version is invalid
    def invalid?
      %w(undefined 0).include?(version.to_s)
    end

    # returns versioned key with prefix
    def versioned_key(key, namespace = '')
      "tml_#{namespace}#{CACHE_VERSION_KEY == key ? '' : "_v#{version}"}_#{key}"
    end

    # returns string representation of the version
    def to_s
      self.version.to_s
    end
  end
end


require 'bdb'
require 'sbdb/db'
require 'sbdb/transaction'

module SBDB
	# Environments are for storing one or more databases and are important
	# if you want to work with more than one process on one database.
	# You needn't use Environment,  but it's usefull.
	class Environment
		INIT_TXN   = Bdb::DB_INIT_TXN
		INIT_LOCK  = Bdb::DB_INIT_LOCK
		INIT_LOG   = Bdb::DB_INIT_LOG
		INIT_MPOOL = Bdb::DB_INIT_MPOOL
		INIT_TRANSACTION = INIT_TXN | INIT_LOCK | INIT_LOG | INIT_MPOOL
		LOCKDOWN   = Bdb::DB_LOCKDOWN
		NOMMAP     = Bdb::DB_NOMMAP
		PRIVATE    = Bdb::DB_PRIVATE
		SYSTEM_MEM = Bdb::DB_SYSTEM_MEM
		TXN_NOSYNC = Bdb::DB_TXN_NOSYNC
		LOG_DIRECT = Bdb::DB_LOG_DIRECT
		LOG_DSYNC  = Bdb::DB_LOG_DSYNC
		LOG_AUTO_REMOVE = Bdb::DB_LOG_AUTO_REMOVE
		LOG_IN_MEMORY = Bdb::DB_LOG_IN_MEMORY
		LOG_ZERO   = Bdb::DB_LOG_ZERO

		# returns the Bdb-object.
		def bdb_object()  @env  end
		# Opens a Btree in this Environment
		def btree file, *ps, &exe
			open Btree, file, *ps, &exe
		end
		# Opens a Hash in this Environment
		def hash file, *ps, &exe
			open Hash, file, *ps, &exe
		end
		# Opens a Recno in this Environment
		def recno file, *ps, &exe
			open Recno, file, *ps, &exe
		end
		# Opens a Queue in this Environment
		def queue file, *ps, &exe
			open Queue, file, *ps, &exe
		end
		# Opens a DB of unknown type in this Environment
		def unknown file, *ps, &exe
			open Unknown, file, *ps, &exe
		end

		def transaction flg = nil, &exe
			SBDB::Transaction.new self, flg, &exe
		end
		alias txn transaction

		# args:
		#   args[0] => dir
		#   args[1] => flags
		#   args[3] => mode
		# possible options (via Hash):
		#	  :dir, :flags, :mode, :log_config
		def initialize *args
			opts = ::Hash === args.last ? args.pop : {}
			opts = {:dir => args[0], :flags => args[1], :mode => args[2]}.update opts
			@dbs, @env = Ref::WeakValueMap.new, Bdb::Env.new( 0)
			@env.log_config opts[:log_config], 1  if opts[:log_config]
			@env.lg_bsize = opts[:lg_bsize]  if opts[:lg_bsize]
			@env.lg_max = opts[:lg_max]  if opts[:lg_max]
			begin @env.open opts[:dir]||'.', opts[:flags]|| INIT_TRANSACTION|CREATE, opts[:mode]||0
			rescue Object
				close
				raise
			end
		end

		def self.new *args
			obj = ret = super( *args)
			begin ret = yield obj
			ensure SBDB::raise_barrier &obj.method(:close)
			end  if block_given?
			ret
		end

		# Close the Environment.
		# First you should close all databases!
		def close
			@dbs.each{|key, db|db.close}
			@env.close
		end

		class << self
			alias open new
		end

		# Opens a Database.
		# see SBDB::DB, SBDB::Btree, SBDB::Hash, SBDB::Recno, SBDB::Queue
		def open type, file, *ps, &exe
			ps.push ::Hash.new  unless ::Hash === ps.last
			ps.last[:env] = self
			(type || SBDB::Unkown).new file, *ps, &exe
		end
		alias db open
		alias open_db open

		# Returns the DB like open, but if it's already opened,
		# it returns the old instance.
		# If you use this, never use close. It's possible somebody else use it too.
		# The Databases, which are opened, will close, if the Environment will close.
		def [] file, *ps, &exe
			opts = ::Hash === ps.last ? ps.pop : {}
			opts[:env] = self
			name, type, flg = ps[0] || opts[:name], ps[1] || opts[:type], ps[2] || opts[:flags]
			ps.push opts
			@dbs[ [file, name, flg | CREATE]] ||= (type || SBDB::Unknown).new file, *ps, &exe
		end
	end
	Env = Environment
end


module Janus
  module Strategies
    extend ActiveSupport::Concern

    # Runs authentication strategies to log a user in.
    def run_strategies(scope)
      Janus::Manager.strategies.each { |name| break if run_strategy(name, scope) }
    end

    # Runs a given strategy and returns true if it succeeded.
    def run_strategy(name, scope)
      strategy = "Janus::Strategies::#{name.to_s.camelize}".constantize.new(scope, self)

      if strategy.valid?
        strategy.authenticate!

        if strategy.success?
          send(strategy.auth_method, strategy.user, :scope => scope)
          Janus::Manager.run_callbacks(:authenticate, strategy.user, self, :scope => scope)
        end
      end

      strategy.success?
    end

    module ClassMethods
      # Returns the list of strategies as underscore symbols.
      def strategies
        @strategies ||= [:rememberable, :remote_authenticatable, :token_authenticatable]
      end
    end
  end
end


module Paraduct
  require "pty"

  class Runner
    attr_reader :params, :base_job_dir

    # @param params [Hash{String => String}] value is quoted (ex. FOO=1 => FOO="1" )
    # @param base_job_dir [String]
    # @param job_id [String]
    def initialize(params: nil, base_job_dir: nil, job_id: nil)
      @params       = params
      @base_job_dir = base_job_dir
      @job_id       = job_id
    end

    def setup_dir
      FileUtils.mkdir_p(job_dir) unless job_dir.exist?
      Paraduct::SyncUtils.copy_recursive(Paraduct.config.root_dir, job_dir)
      Dir.chdir(job_dir)
    end

    # run script with params
    # @param script [String, Array<String>] script file, script(s)
    # @return [String] stdout
    # @raise [Paraduct::Errors::ProcessError] command exited error status
    def perform(script)
      export_variables = @params.reverse_merge("PARADUCT_JOB_ID" => @job_id, "PARADUCT_JOB_NAME" => job_name)
      variable_string = export_variables.map { |key, value| %(export #{key}="#{value}";) }.join(" ")

      Array.wrap(script).inject("") do |stdout, command|
        stdout << run_command("#{variable_string} #{command}")
        stdout
      end
    end

    def job_dir
      Pathname(@base_job_dir).join(job_name)
    end

    def job_name
      @params.map { |key, value| "#{key}_#{value}" }.join("_").gsub(%r{[/ ]}, "_")
    end

    def formatted_params
      @params.map { |key, value| "#{key}=#{value}" }.join(", ")
    end

    def logger
      unless @logger
        stdout_logger = Paraduct::ColoredLabelLogger.new(formatted_params)
        file_logger   = Logger.new(Pathname(@base_job_dir).join("#{job_name}.log"))
        @logger       = stdout_logger.extend(ActiveSupport::Logger.broadcast(file_logger))
      end

      @logger
    end

    private

      def run_command(command)
        full_stdout = ""
        exit_status = nil

        logger.info "run_command: #{command}"

        PTY.spawn(command) do |stdin, stdout, pid|
          stdout.close_write
          stdin.sync = true

          begin
            stdin.each do |line|
              line.strip!
              logger.info line
              full_stdout << "#{line}\n"
            end
          rescue Errno::EIO # rubocop:disable Lint/HandleExceptions
          ensure
            _, exit_status = Process.waitpid2(pid)
          end
        end

        raise Paraduct::Errors::ProcessError.new(full_stdout, exit_status) unless exit_status.success?

        full_stdout
      end
  end
end


module Gliffy
  class Account
    TYPE_BUSINESS = "Business"

    attr_reader :api
    attr_reader :id
    attr_reader :name
    attr_reader :max_users, :type, :terms_accepted
    attr_reader :expiration_date

    def self.load(api, response)
      Gliffy::Account.new(
        api,
        :id => response.integer('//g:account/@id'),
        :name => response.string('//g:account/g:name'),
        :max_users => response.integer('//g:account/@max-users'),
        :type => response.string('//g:account/@account-type'),
        :terms_accepted => response.string('//g:account/@terms') == "true",
        :expiration_date => response.timestamp('//g:account/g:expiration-date')
      )
    end

    def root
      @root ||= load_root
    end

    def users
      @users ||= load_users
    end

    def document(document_id)
      response = api.get("/accounts/#{id}/documents/#{document_id}/meta-data.xml",
                         :action => 'get')

      Gliffy::Document.load(
        self,
        response.node('//g:document')
      )
    end

    def create_user(username)
      if username =~ /\s/ then
        raise ArgumentError.new(username)
      end

      if users.select { |u| u.username == username }.length > 0 then
        raise ArgumentError.new(username)
      end

      api.create_user(username)
      users.select { |u| u.username == username }.first
    end

    # observer callback
    def update(event, target)
      case event
      when :user_deleted
        @users = @users.delete_if { |element| element == target }
        target.delete_observer(self)
      else
        raise ArgumentError.new(event)
      end
    end

    private

    def initialize(api, params)
      @api = api

      @id = params[:id]
      @name = params[:name]
      @max_users = params[:max_users]
      @type = params[:type]
      @terms_accepted = params[:terms_accepted]
      @expiration_date = params[:expiration_date]
    end

    def load_root
      response = api.get_folders(id)

      Gliffy::Folder.load(
        self,
        response.node("/g:response/g:folders/g:folder")
      )
    end

    def load_users
      api.get_users(id)
        .nodes('//g:user')
        .map { |n| load_user n }
    end

    def load_user(node)
      Gliffy::User.load(self, node)
    end
  end
end


# encoding: UTF-8
require "rails/generators"

module Abstractor
  class InstallGenerator < Rails::Generators::Base
    class_option "no-migrations", :type => :boolean
    class_option "customize-all", :type => :boolean
    class_option "customize-controllers", :type => :boolean
    class_option "customize-models", :type => :boolean
    class_option "customize-helpers", :type => :boolean
    class_option "customize-layout", :type => :boolean
    class_option "current-user-helper", :type => :string
    class_option "no-install-stanford-core-nlp", :type => :boolean

    def self.source_paths
      paths = self.superclass.source_paths
      paths << File.expand_path("../templates", __FILE__)
      paths.flatten
    end

    desc "Used to install Abstractor"

    def install_migrations
      unless options["no-migrations"]
        puts "Copying over Abstractor migrations..."
        Dir.chdir(Rails.root) do
          `rake abstractor:install:migrations`
        end
      end
    end

    ## shameless steal from forem git://github.com/radar/forem.git
    def add_abstractor_user_method
      current_user_helper = options["current-user-helper"].presence ||
                            ask("What is the current_user helper called in your app? [current_user]").presence ||
                            'current_user if defined?(current_user)'
      puts "Defining abstractor_user method inside ApplicationController..."

      abstractor_user_method = %Q{
  def abstractor_user
    #{current_user_helper}
  end
  helper_method :abstractor_user
}
      inject_into_file("#{Rails.root}/app/controllers/application_controller.rb",
                       abstractor_user_method,
                       :after => "ActionController::Base\n")
    end

    def mount_engine
      puts "Mounting Abstractor::Engine at \"/\" in config/routes.rb..."
      insert_into_file("#{Rails.root}/config/routes.rb", :after => /routes.draw.do\n/) do
        %Q{  mount Abstractor::Engine, :at => "/"\n}
      end
    end

    def setup_stanford_core_nlp
      unless options["no-install-stanford-core-nlp"]
        puts "Running rake abstractor:setup:stanford_core_nlp"
        puts 'Please be patient...this could take a while.'
        `rake abstractor:setup:stanford_core_nlp`

        insert_into_file("#{Rails.root}/config/environments/development.rb", :after => /(::Application.configure do\n|application.configure do\n)/) do
'  StanfordCoreNLP.use :english
  StanfordCoreNLP.model_files = {}
  StanfordCoreNLP.jar_path = "#{Rails.root}/lib/stanford-corenlp-full-2015-04-20/"
  StanfordCoreNLP.model_path = "#{Rails.root}/lib/stanford-corenlp-full-2015-04-20/"
  StanfordCoreNLP.default_jars = [
    "joda-time.jar",
    "xom.jar",
    "stanford-corenlp-3.5.2.jar",
    "stanford-corenlp-3.5.2-models.jar",
    "jollyday.jar",
    "bridge.jar"
  ]
'
        end
      end
    end

    def make_customizable
      if options["customize-all"] || options["customize-controllers"]
        path = "#{Rails.root}/app/controllers/abstractor"
        empty_directory path
        copy_file "controllers/abstractor_abstraction_groups_controller.rb", "#{path}/abstractor_abstraction_groups_controller.rb"
        copy_file "controllers/abstractor_abstractions_controller.rb", "#{path}/abstractor_abstractions_controller.rb"
        copy_file "controllers/abstractor_suggestions_controller.rb", "#{path}/abstractor_suggestions_controller.rb"
        copy_file "controllers/abstractor_abstraction_schemas_controller.rb", "#{path}/abstractor_abstraction_schemas_controller.rb"
      end

      if options["customize-all"] || options["customize-helpers"]
        path = "#{Rails.root}/app"
        empty_directory "#{path}/helpers/abstractor"
        copy_file "helpers/abscractions_helper.rb", "#{path}/helpers/abstractor/abscractions_helper.rb"
      end

      if options["customize-all"] || options["customize-models"]
        path = "#{Rails.root}/app"
        empty_directory "#{path}/models/abstractor"
        copy_file "models/abstractor_abstraction_group_member.rb", "#{path}/models/abstractor/abstractor_abstraction_group_member.rb"
        copy_file "models/abstractor_abstraction_group.rb", "#{path}/models/abstractor/abstractor_abstraction_group.rb"
        copy_file "models/abstractor_abstraction_schema_object_value.rb", "#{path}/models/abstractor/abstractor_abstraction_schema_object_value.rb"
        copy_file "models/abstractor_abstraction_schema_predicate_variant.rb", "#{path}/models/abstractor/abstractor_abstraction_schema_predicate_variant.rb"
        copy_file "models/abstractor_abstraction_schema_relation.rb", "#{path}/models/abstractor/abstractor_abstraction_schema_relation.rb"
        copy_file "models/abstractor_abstraction_schema.rb", "#{path}/models/abstractor/abstractor_abstraction_schema.rb"
        copy_file "models/abstractor_abstraction_source.rb", "#{path}/models/abstractor/abstractor_abstraction_source.rb"
        copy_file "models/abstractor_abstraction.rb", "#{path}/models/abstractor/abstractor_abstraction.rb"
        copy_file "models/abstractor_object_type.rb", "#{path}/models/abstractor/abstractor_object_type.rb"
        copy_file "models/abstractor_object_value_variant.rb", "#{path}/models/abstractor/abstractor_object_value_variant.rb"
        copy_file "models/abstractor_object_value.rb", "#{path}/models/abstractor/abstractor_object_value.rb"
        copy_file "models/abstractor_relation_type.rb", "#{path}/models/abstractor/abstractor_relation_type.rb"
        copy_file "models/abstractor_rule_type.rb", "#{path}/models/abstractor/abstractor_rule_type.rb"
        copy_file "models/abstractor_subject_group_member.rb", "#{path}/models/abstractor/abstractor_subject_group_member.rb"
        copy_file "models/abstractor_subject_group.rb", "#{path}/models/abstractor/abstractor_subject_group.rb"
        copy_file "models/abstractor_subject_relation.rb", "#{path}/models/abstractor/abstractor_subject_relation.rb"
        copy_file "models/abstractor_subject.rb", "#{path}/models/abstractor/abstractor_subject.rb"
        copy_file "models/abstractor_suggestion_object_value.rb", "#{path}/models/abstractor/abstractor_suggestion_object_value.rb"
        copy_file "models/abstractor_suggestion_source.rb", "#{path}/models/abstractor/abstractor_suggestion_source.rb"
        copy_file "models/abstractor_suggestion_status.rb", "#{path}/models/abstractor/abstractor_suggestion_status.rb"
        copy_file "models/abstractor_suggestion_status.rb", "#{path}/models/abstractor/abstractor_suggestion.rb"
      end
    end
  end
end


require 'uri'

module Gliffy
  class Folder
    include Observable

    attr_reader :name, :path
    attr_reader :owner, :parent
    attr_reader :folders

    def self.load(owner, node)
      Gliffy::Folder.new(
        owner,
        node.string('g:name'),
        node.string('g:path'),
        node.nodes('g:folder').map {|n| Gliffy::Folder.load(owner, n)}
      )
    end

    def initialize(owner, name, path, folders)
      @owner = owner
      @name = name
      @path = path

      @folders = folders
      @folders.each do |f|
        f.parent = self
        f.add_observer(self)
      end

      @is_deleted = false
    end

    def users
      api.users_with_access_to_folder(path)
        .nodes("//g:user")
        .map { |n| Gliffy::User.load(owner, n) }
    end

    def grant_access(user)
      api.grant_access_to_folder(user.username, path)
    end

    def revoke_access(user)
      api.revoke_access_to_folder(user.username, path)
    end

    def parent=(parent)
      if path != parent.path + "/" + name then
        raise "Invalid parent"
      end

      @parent = parent
    end

    def create_document(name)
      api.create_document(name, Gliffy::Document::TYPE_DIAGRAM, nil, path)
    end

    def create_folder(name)
      if has_child? name
        raise ArgumentError.new(name)
      end

      new_path = path + "/" + name

      api.create_folder(new_path)

      new_folder = Folder.new(owner, name, new_path, [])
      folders.push(new_folder)
      new_folder
    end

    def documents
      @documents ||= load_documents
    end

    def root?
      path == "ROOT"
    end

    def deleted?
      @is_deleted
    end

    def has_child?(name)
      normalized_name = name.downcase
      not folders.index do |child|
        child.name.downcase == normalized_name
      end.nil?
    end

    # observer callback
    def update(event, target)
      case event
      when :document_removed, :document_deleted
        @documents = @documents.delete_if { |element| element == target }
        target.delete_observer(self)
      when :document_added
        @documents.push target
        target.add_observer(self)
      when :folder_deleted
        @folders = @folders.delete_if { |element| element == target }
      else
        raise ArgumentError.new(event)
      end
    end

    def delete
      api.delete_folder(path)
      @is_deleted = true

      changed
      notify_observers :folder_deleted, self
    end

    private

    def api
      owner.api
    end

    def load_documents
      response = api.get_documents_in_folder(path)

      response
        .nodes('//g:document')
        .map { |n| load_document n }
    end

    def load_document(n)
      document = Gliffy::Document.load(owner, n)
      document.add_observer(self)
      document
    end
  end
end


module Sixword

  # The Sixword::CLI class implements all of the complex processing needed for
  # the sixword Command Line Interface.
  class CLI

    # Exception for certain input validation errors
    class CLIError < StandardError; end

    # @return [String] Input filename
    attr_reader :filename

    # @return [Hash] Options hash
    attr_reader :options

    # @return [File, IO] Stream opened from #filename
    attr_reader :stream

    # @return [:encode, :decode]
    attr_reader :mode

    # Create a Sixword CLI to operate on filename with options
    #
    # @param filename [String] Input file name (or '-' for stdin)
    # @param options [Hash]
    #
    # @option options [:encode, :decode] :mode (:encode)
    # @option options [Boolean] :pad (false)
    # @option options [String] :hex_style
    # @option options [Integer] :line_width (1) In encode mode, the number of
    #   sentences to output per line.
    #
    def initialize(filename, options)
      @filename = filename
      @options = {mode: :encode, pad: false}.merge(options)

      if filename == '-'
        # dup stdin and put it into binary mode
        @stream = $stdin.dup
        @stream.binmode
      else
        # read in binary mode even on unix so ruby yields binary encoding
        @stream = File.open(filename, 'rb')
      end

      @mode = @options.fetch(:mode)
      unless [:encode, :decode].include?(mode)
        raise ArgumentError.new("Invalid mode: #{mode.inspect}")
      end
    end

    # Return the value of the :pad option.
    # @return [Boolean]
    def pad?
      options.fetch(:pad)
    end

    # Return true if we are in encoding mode, false otherwise (decoding).
    # @return [Boolean]
    def encoding?
      mode == :encode
    end

    # Return the value of the :hex_style option.
    # @return [String, nil]
    def hex_style
      options[:hex_style]
    end

    # Format data as hex in various styles.
    def print_hex(data, chunk_index, cols=80)
      case hex_style
      when 'lower', 'lowercase'
        # encode to lowercase hex with no newlines
        print Sixword::Hex.encode(data)
      when 'finger', 'fingerprint'
        # encode to GPG fingerprint like hex with newlines
        newlines_every = cols / 5
        if chunk_index != 0
          if chunk_index % newlines_every == 0
            print "\n"
          else
            print ' '
          end
        end
        print Sixword::Hex.encode_fingerprint(data)
      when 'colon', 'colons'
        # encode to SSL/SSH fingerprint like hex with colons
        print ':' unless chunk_index == 0
        print Sixword::Hex.encode_colons(data)
      end
    end

    # Run the encoding/decoding operation, printing the result to stdout.
    def run!
      if encoding?
        do_encode! do |encoded|
          puts encoded
        end
      else
        chunk_index = 0
        do_decode! do |decoded|
          if hex_style
            print_hex(decoded, chunk_index)
            chunk_index += 1
          else
            print decoded
          end
        end

        # add trailing newline for hex output
        puts if hex_style
      end
    end

    private

    def do_decode!
      unless block_given?
        raise ArgumentError.new("block is required")
      end

      read_input_by_6_words do |arr|
        yield Sixword.decode(arr, padding_ok: pad?)
      end
    end

    def do_encode!
      sentences_per_line = options.fetch(:line_width, 1)
      if sentences_per_line <= 0
        sentences_per_line = 1 << 32
      end

      sentences = []

      process_encode_input do |chunk|
        Sixword.encode_iter(chunk, words_per_slice:6, pad:pad?) do |encoded|
          sentences << encoded

          # yield sentences once we reach sentences_per_line of them
          if sentences.length >= sentences_per_line
            yield sentences.join(' ')
            sentences.clear
          end
        end
      end

      # yield any leftover sentences
      unless sentences.empty?
        yield sentences.join(' ')
      end
    end

    def process_encode_input
      unless block_given?
        raise ArgumentError.new("block is required")
      end

      if hex_style
        # yield data in chunks from accumulate_hex_input until EOF
        accumulate_hex_input do |hex|
          begin
            data = Sixword::Hex.decode(hex)
          rescue ArgumentError => err
            # expose hex decoding failures to user
            raise CLIError.new(err.message)
          else
            yield data
          end
        end
      else
        # yield data 8 bytes at a time until EOF
        while true
          buf = stream.read(8)
          if buf
            yield buf
          else
            # EOF
            break
          end
        end
      end
    end

    # Yield data 6 words at a time until EOF
    def read_input_by_6_words
      word_arr = []

      while true
        line = stream.gets
        if line.nil?
          break # EOF
        end

        line.scan(/\S+/) do |word|
          word_arr << word

          # return the array if we have accumulated 6 words
          if word_arr.length == 6
            yield word_arr
            word_arr.clear
          end
        end
      end

      # yield whatever we have left, if anything
      if !word_arr.empty?
        yield word_arr
      end
    end

    def accumulate_hex_input
      unless block_given?
        raise ArgumentError.new("must pass block")
      end

      # these are actually treated the same at the moment
      case hex_style
      when 'lower', 'lowercase'
      when 'finger', 'fingerprint'
      else
        raise CLIError.new("unknown hex style: #{hex_style.inspect}")
      end

      while true
        buf = ''

        # try to accumulate 8 bytes (16 chars) before yielding the hex
        while buf.length < 16
          char = stream.getc
          if char.nil?
            # EOF, so yield whatever we have if it's non-empty
            yield buf unless buf.empty?
            return
          end

          if Sixword::Hex.valid_hex?(char)
            buf << char
            next
          elsif Sixword::Hex.strip_char?(char)
            next
          end

          raise CLIError.new("invalid hex character: #{char.inspect}")
        end

        yield buf
      end
    end
  end
end


# encoding: utf-8

##
# Oedipus Sphinx 2 Search.
# Copyright © 2012 Chris Corbyn.
#
# See LICENSE file for details.
##

module Oedipus
  # Constructs SphinxQL queries from the internal Hash format.
  class QueryBuilder
    # Initialize a new QueryBuilder for +index_name+.
    #
    # @param [Symbol] index_name
    #   the name of the index being queried
    def initialize(index_name)
      @index_name = index_name
    end

    # Build a SphinxQL query for the fulltext search +query+ and filters in +filters+.
    #
    # @param [String] query
    #   the fulltext query to execute (may be empty)
    #
    # @param [Hash] filters
    #   additional attribute filters and other options
    #
    # @return [String]
    #   a SphinxQL query
    def select(query, filters)
      where, *bind_values = conditions(query, filters)
      [
        [
          from(filters),
          where,
          order_by(filters),
          limits(filters)
        ].join(" "),
        *bind_values
      ]
    end

    # Build a SphinxQL query to insert the record identified by +id+ with the given attributes.
    #
    # @param [Fixnum] id
    #   the unique ID of the document to insert
    #
    # @param [Hash] attributes
    #   a Hash of attributes
    #
    # @return [String]
    #   the SphinxQL to insert the record
    def insert(id, attributes)
      into("INSERT", id, attributes)
    end

    # Build a SphinxQL query to update the record identified by +id+ with the given attributes.
    #
    # @param [Fixnum] id
    #   the unique ID of the document to update
    #
    # @param [Hash] attributes
    #   a Hash of attributes
    #
    # @return [String]
    #   the SphinxQL to update the record
    def update(id, attributes)
      set_attrs, *bind_values = update_attributes(attributes)
      [
        [
          "UPDATE #{@index_name} SET",
          set_attrs,
          "WHERE id = ?"
        ].join(" "),
        *bind_values.push(id)
      ]
    end

    # Build a SphinxQL query to replace the record identified by +id+ with the given attributes.
    #
    # @param [Fixnum] id
    #   the unique ID of the document to replace
    #
    # @param [Hash] attributes
    #   a Hash of attributes
    #
    # @return [String]
    #   the SphinxQL to replace the record
    def replace(id, attributes)
      into("REPLACE", id, attributes)
    end

    # Build a SphinxQL query to delete the record identified by +id+.
    #
    # @param [Fixnum] id
    #   the unique ID of the document to delete
    #
    # @return [String]
    #   the SphinxQL to delete the record
    def delete(id)
      ["DELETE FROM #{@index_name} WHERE id = ?", id]
    end

    private

    RESERVED = [:attrs, :limit, :offset, :order]

    def fields(filters)
      filters.fetch(:attrs, [:*]).dup.tap do |fields|
        if fields.none? { |a| /\brelevance\n/ === a } && normalize_order(filters).key?(:relevance)
          fields << "WEIGHT() AS relevance"
        end
      end
    end

    def from(filters)
      [
        "SELECT",
        fields(filters).join(", "),
        "FROM",
        @index_name
      ].join(" ")
    end

    def into(type, id, attributes)
      attrs, values = attributes.inject([[:id], [id]]) do |(a, b), (k, v)|
        [a.push(k), b.push(v)]
      end

      [
        [
          type,
          "INTO #{@index_name}",
          "(#{attrs.join(', ')})",
          "VALUES",
          "(#{(['?'] * attrs.size).join(', ')})"
        ].join(" "),
        *values
      ]
    end

    def conditions(query, filters)
      sql = []
      sql << ["MATCH(?)", query] unless query.empty?
      sql.push(*attribute_conditions(filters))

      exprs, bind_values = sql.inject([[], []]) do |(strs, values), v|
        [strs.push(v.shift), values.push(*v)]
      end

      ["WHERE " << exprs.join(" AND "), *bind_values] if exprs.any?
    end

    def attribute_conditions(filters)
      filters.reject{ |k, v| RESERVED.include?(k.to_sym) }.map do |k, v|
        Comparison.of(v).to_sql.tap { |c| c[0].insert(0, "#{k} ") }
      end
    end

    def update_attributes(attributes)
      [
        attributes.keys.map{ |k| "#{k} = ?" }.join(", "),
        *attributes.values
      ]
    end

    def order_by(filters)
      return unless (order = normalize_order(filters)).any?

      [
        "ORDER BY",
        order.map { |k, dir| "#{k} #{dir.to_s.upcase}" }.join(", ")
      ].join(" ")
    end

    def normalize_order(filters)
      Hash[Array(filters[:order]).map { |k, v| [k.to_sym, v || :asc] }]
    end

    def limits(filters)
      "LIMIT #{filters[:offset].to_i}, #{filters[:limit].to_i}" if filters.key?(:limit)
    end
  end
end


# encoding: utf-8

##
# Oedipus Sphinx 2 Search.
# Copyright © 2012 Chris Corbyn.
#
# See LICENSE file for details.
##

module Oedipus
  # Provides an interface for talking to SphinxQL.
  #
  # Currently this class wraps a native mysql extension.
  class Connection
    attr_reader :options

    # Instantiate a new Connection to a SphinxQL host.
    #
    # @param [String] server
    #   a 'hostname:port' string
    #
    # @param [Hash] options
    #   a Hash containing :host and :port
    #
    # The connection will be established on initialization.
    #
    # The underlying implementation uses a thread-safe connection pool.
    def initialize(options)
      @options =
        if options.kind_of?(String)
          Hash[ [:host, :port].zip(options.split(":")) ]
        else
          options.dup
        end.tap { |o| o[:port] = o[:port].to_i }

      @pool = Pool.new(
        host: @options[:host],
        port: @options[:port],
        size: @options.fetch(:pool_size, 8),
        ttl:  60
      )

      assert_valid_pool unless @options[:verify] == false
    end

    # Acess a specific index for querying.
    #
    # @param [String] index_name
    #   the name of an existing index in Sphinx
    #
    # @return [Index]
    #   an index that can be queried
    def [](index_name)
      Index.new(index_name, self)
    end

    alias_method :index, :[]

    # Execute one or more queries in a batch.
    #
    # Queries should be separated by semicolons.
    # Results are returned in a 2-dimensional array.
    #
    # @param [String] sql
    #   one or more SphinxQL statements, separated by semicolons
    #
    # @param [Object...] bind_values
    #   values to be substituted in place of '?' in the query
    #
    # @return [Array]
    #   an array of arrays, containing the returned records
    #
    # Note that SphinxQL does not support prepared statements.
    def multi_query(sql, *bind_values)
      @pool.acquire { |conn| conn.query(sql, *bind_values) }
    end

    # Execute a single read query.
    #
    # @param [String] sql
    #   a single SphinxQL statement
    #
    # @param [Object...] bind_values
    #   values to be substituted in place of '?' in the query
    #
    # @return [Array]
    #   an array of Hashes containing the matched records
    #
    # Note that SphinxQL does not support prepared statements.
    def query(sql, *bind_values)
      @pool.acquire { |conn| conn.query(sql, *bind_values).first }
    end

    # Execute a non-read query.
    #
    # @param [String] sql
    #   a SphinxQL query, such as INSERT or REPLACE
    #
    # @param [Object...] bind_values
    #   values to be substituted in place of '?' in the query
    #
    # @return [Fixnum]
    #   the number of affected rows
    #
    # Note that SphinxQL does not support prepared statements.
    def execute(sql, *bind_values)
      @pool.acquire { |conn| conn.execute(sql, *bind_values) }
    end

    # Disconnect from the remote host.
    #
    # There is no need to explicitly re-connect after invoking this;
    # connections are re-established as needed.
    def close
      @pool.dispose
    end

    private

    def assert_valid_pool
      @pool.acquire { nil }
    end
  end
end


#
# Classes which include this module become automatically visible to
# MixinRegistry (the default way of finding jobs in Woodhouse).
# All public methods of the class are automatically made available as
# jobs.
#
# Classes including Woodhouse::Worker also get access to the +logger+
# method, which will be the same logger globally configured for the current
# Layout.
#
# Classes including Woodhouse::Worker also have convenience shortcuts
# for dispatching jobs. Any job defined on the class can be dispatched
# asynchronously by calling ClassName.async_job_name(options).
#
# == Example
#
#   class PamPoovey
#     include Woodhouse::Worker
#
#     # This is available as the job PamPoovey#do_hr
#     def do_hr(options)
#       logger.info "Out comes the dolphin puppet"
#     end
#
#     private
#
#     # This is not picked up as a job
#     def fight_club
#       # ...
#     end
#   end
#
#   # later ...
#
#   Woodhouse::MixinRegistry.new[:PamPoovey] # => PamPoovey
#   PamPoovey.async_do_hr(:employee => "Lana")
#
module Woodhouse::Worker

  def self.included(into)
    into.extend ClassMethods
    into.set_worker_name into.name unless into.name.nil?
  end

  # The current Woodhouse logger. Set by the runner. Don't expect it to be set
  # if you create the object yourself. If you want to be able to run job methods
  # directly, you should account for setting +logger+.
  attr_accessor :logger

  module ClassMethods

    def worker_name
      @worker_name
    end

    # Sets the name for this worker class if not already set (i.e., if it's
    # an anonymous class). The first time the name for the worker is set,
    # it becomes registered with MixinRegistry. After that, attempting to
    # change the worker class will raise ArgumentError.
    def set_worker_name(name)
      if @worker_name
        raise ArgumentError, "cannot change worker name"
      else
        if name and !name.empty?
          @worker_name = name.to_sym
          Woodhouse::MixinRegistry.register self
        end
      end
    end

    def available_jobs
      @available_jobs ||= public_instance_methods(false)
    end

    def only_jobs(*jobs)
      @available_jobs = jobs
    end

    def exclude_jobs(*jobs)
      @available_jobs -= jobs
    end

    # You can dispatch a job +baz+ on class +FooBar+ by calling FooBar.async_baz.
    def method_missing(method, *args, &block)
      if method.to_s =~ /^asynch?_(.*)/
        if instance_methods(false).detect{|meth| meth.to_s == $1 }
          Woodhouse.dispatch(@worker_name, $1, args.first)
        else
          super
        end
      else
        super
      end
    end

  end

end


module Woodhouse

  #
  # A Layout describes the configuration of a set of Woodhouse Server instances.
  # Each Server runs all of the workers assigned to a single Node.
  #
  # Layouts and their contents (Node and Worker instances) are all plain data,
  # suitable to being serialized, saved out, passed around, etc.
  #
  # Woodhouse clients do not need to know anything about the Layout to dispatch
  # jobs, but servers rely on the Layout to know which jobs to serve. The basic
  # process of setting up a Woodhouse server is to create a layout with one or
  # more nodes and then pass it to Woodhouse::Server to serve.
  #
  # There is a default layout suitable for many applications, available as
  # Woodhouse::Layout.default. It has a single node named :default, which has
  # the default node configuration -- one worker for every job. If you do not
  # need to distribute different sets of jobs to different workers, the default
  # layout should serve you.
  #
  # TODO: A nicer DSL for creating and tweaking Layouts.
  #
  class Layout
    include Woodhouse::Util

    def initialize
      @nodes = []
    end

    # Returns a frozen list of the nodes assigned to this layout.
    def nodes
      @nodes.frozen? ? @nodes : @nodes.dup.freeze
    end

    # Adds a Node to this layout. If +node+ is a Symbol, a Node will be
    # automatically created with that name.
    #
    #   # Example:
    #
    #   layout.add_node Woodhouse::Layout::Node.new(:isis)
    #
    #   # Is equivalent to
    #
    #   layout.add_node :isis
    #
    def add_node(node)
      if node.respond_to?(:to_sym)
        node = Woodhouse::Layout::Node.new(node.to_sym)
      end
      expect_arg :node, Woodhouse::Layout::Node, node
      @nodes << node
      node
    end

    # Looks up a Node by name and returns it.
    def node(name)
      name = name.to_sym
      @nodes.detect{|node|
        node.name == name
      }
    end

    # Returns a frozen copy of this Layout and all of its child Node and
    # Worker objects. Woodhouse::Server always takes a frozen copy of the
    # layout it is given. It is thus safe to modify the same layout
    # subsequently, and the changes only take effect when the layout is
    # passed to the server again and Woodhouse::Server#reload is called.
    def frozen_clone
      clone.tap do |cloned|
        cloned.nodes = @nodes.map{|node| node.frozen_clone }.freeze
        cloned.freeze
      end
    end

    # Returns a set of Changes necessary to move from +other_layout+ to this
    # layout. This is used to permit live reconfiguration of servers by only
    # spinning up and down nodes/workers which have changed.
    def changes_from(other_layout, node)
      Woodhouse::Layout::Changes.new(self, other_layout, node)
    end

    def dump(serializer = Woodhouse::LayoutSerializer)
      serializer.dump(self)
    end

    def self.load(dumped, serializer = Woodhouse::LayoutSerializer)
      serializer.load(dumped)
    end

    # The default layout, for convenience purposes. Has one node +:default+,
    # which has the default configuration (see Woodhouse::Layout::Node#default_configuration!)
    def self.default
      new.tap do |layout|
        layout.add_node :default
        layout.node(:default).default_configuration!(Woodhouse.global_configuration)
      end
    end

    protected

    attr_writer :nodes

    #
    # A Node describes the set of workers present on a single Server.
    #
    # More information about Woodhouse's layout system can be found in the
    # documentation for Woodhouse::Layout.
    #
    class Node
      include Woodhouse::Util

      attr_reader :name

      def initialize(name)
        @name = name.to_sym
        @workers = []
      end

      # Returns a frozen list of workers assigned to this node.
      def workers
        @workers.frozen? ? @workers : @workers.dup.freeze
      end

      # Adds a Worker to this node.
      def add_worker(worker)
        expect_arg :worker, Woodhouse::Layout::Worker, worker
        @workers << worker
      end

      def remove_worker(worker)
        expect_arg :worker, Woodhouse::Layout::Worker, worker
        @workers.delete(worker)
      end

      def worker_for_job(job)
        @workers.detect {|worker|
          worker.accepts_job?(job)
        }
      end

      def clear
        @workers.clear
      end

      # Configures this node with one worker per job (jobs obtained
      # from Registry#each). The +default_threads+ value of the given
      # +config+ is used to determine how many threads should be
      # assigned to each worker.
      def default_configuration!(config, options = {})
        options[:threads] ||= config.default_threads
        config.registry.each do |name, klass|
          klass.available_jobs.each do |method|
            add_worker Woodhouse::Layout::Worker.new(name, method, options)
          end
        end
      end

      # Used by Layout#frozen_clone
      def frozen_clone # :nodoc:
        clone.tap do |cloned|
          cloned.workers = @workers.map{|worker| worker.frozen_clone }.freeze
          cloned.freeze
        end
      end

      protected

      attr_writer :workers
    end

    #
    # A Worker describes a single job that is performed on a Server.
    # One or more Runner actors are created for every Worker in a Node.
    #
    # Any Worker has three parameters used to route jobs to it:
    #
    # +worker_class_name+::
    #   This is generally a class name. It's looked up
    #   in a Registry and used to instantiate a job object.
    # +job_method+::
    #   This is a method on the object called up with +worker_class_name+.
    # +criteria+::
    #   A hash of values (actually, a QueueCriteria object) used
    #   to filter only specific jobs to this worker. When a job is dispatched,
    #   its +arguments+ are compared with a worker's +criteria+. This is
    #   done via an AMQP headers exchange (TODO: need to have a central document
    #   to reference on how Woodhouse uses AMQP and jobs are dispatched)
    #
    class Worker
      attr_reader :worker_class_name, :job_method, :threads, :criteria
      attr_accessor :flags

      def initialize(worker_class_name, job_method, opts = {})
        opts = opts.clone
        self.worker_class_name = worker_class_name
        self.job_method = job_method
        self.threads   = opts.delete(:threads) || 1
        criteria  = opts.delete(:only)
        self.flags     = opts
        self.criteria  = criteria
      end

      def exchange_name
        "#{worker_class_name}_#{job_method}".downcase
      end

      def queue_name
        exchange_name + criteria.queue_key
      end

      def worker_class_name=(value)
        @worker_class_name = value.to_sym
      end

      def job_method=(value)
        @job_method = value.to_sym
      end

      def threads=(value)
        @threads = value.to_i
      end

      def criteria=(value)
        @criteria = Woodhouse::QueueCriteria.new(value, flags).freeze
      end

      def frozen_clone
        clone.freeze
      end

      def describe
        "#@worker_class_name##@job_method(#{@criteria.describe})"
      end

      def accepts_job?(job)
        criteria.matches?(job.arguments)
      end

      # TODO: want to recognize increases and decreases in numbers of
      # threads and make minimal changes
      def ==(other)
        [worker_class_name, job_method,
          threads, criteria] ==
        [other.worker_class_name, other.job_method,
          other.threads, other.criteria]
      end

    end

    #
    # A diff between two Layouts, used to determine what workers need to be
    # spun up and down when a layout change is sent to a Server.
    #
    class Changes

      def initialize(new_layout, old_layout, node_name)
        @new_layout = new_layout
        @new_node = @new_layout && @new_layout.node(node_name)
        @old_layout = old_layout
        @old_node = @old_layout && @old_layout.node(node_name)
        @node_name = node_name
      end

      def adds
        new_workers.reject{|worker|
          old_workers.member? worker
        }
      end

      def drops
        old_workers.reject{|worker|
          new_workers.member? worker
        }
      end

      private

      def old_workers
        @old_workers ||= @old_node ? @old_node.workers : []
      end

      def new_workers
        @new_workers ||= @new_node ? @new_node.workers : []
      end

    end

  end

end