# == Schema Information
# Schema version: 51
#
# Table name: tagset
#
#  tagset_id  :integer         primary key
#  typ        :text            
#  lewe       :text            
#  prawe      :text            
#  created_at :timestamp       
#  updated_at :timestamp       
#

 #
 # This file is part of the Anotatornia suite.
 # 
 # Copyright © 2007, 2008, 2009, 2010 by Instytut Podstaw Informatyki
 # Polskiej Akademii Nauk (IPI PAN; Institute of Computer Science, Polish
 # Academy of Sciences; cf. www.ipipan.waw.pl).  All rights reserved.
 # 
 # This file may be distributed and/or modified under the terms of the
 # GNU General Public License version 3 as published by the Free Software
 # Foundation and appearing in the file COPYING included in the packaging
 # of this file.  (See http://www.gnu.org/licenses/translations.html for
 # unofficial translations.)
 # 
 # A commercial license is available from IPI PAN (contact
 # Michal.Ciesiolka.waw.pl or ipi.waw.pl for more
 # information).  Licensees holding a valid commercial license from IPI
 # PAN may use this file in accordance with that license.
 # 
 # This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING
 # THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 # PURPOSE.
 #

# Tabela ta jest poniekąd polimorficzna: zawiera różne typy reguł (aliases, counteralias, attr, counterattr, pos),  a ich obiekty »prawe« są odpowiednich (różnych) typów:
#
# dla »aliases« — regexp klas gramatycznych danej części mowy,
# dla »counteralias« — string część mowy danej klasy gramatycznej; niejednoznaczne dla »ger«,
# dla »attr« — tablica wartości danego atrybutu,
# dla »counterattr« — string atrybut danej wartości, w~Tagsecie \ac{NKJP} 0.2 mocno niejednoznaczne
# dla »pos« tablica przysługujących atrybutów postaci [atryb] lub [atryb, :optional].

require 'ramkowanie'

class Tagset < ActiveRecord::Base
  serialize :prawe


  def self.zainicjuj( force=nil )
    if force or not self.find( :first )
      ##      self.connection.execute "delete from tagset "
      Tagset.delete_all
      require 'ftools' # for File.copy etc.
      tagsetname='gramdata/tagset.cfg'
      ##    self.connection.execute("pragma synchronous=off;")
      currtype = nil
      
      nowytyp = Proc.new { |match|
        currtype = match.downcase }
          
      typ_dobry = Proc.new {
        %w( aliases attr pos ).include?(  currtype ) }
      
      File.open(tagsetname) {|f| 
        f.each {|line| 
          line.chomp!.strip!.downcase! # the last added 2010/6/16 while fixing \#356.
          
          case line
          when /^\[(.+)\]$/
            match = $1
            nowytyp.call( match )
            puts currtype
            
          when /^[a-z].*=/
            if typ_dobry.call
              # wpisujemy do tabeli w~bazie danych
              lewe, prawe =  line.split( '=' ).collect{|x| x.strip }
              case currtype
              when 'aliases' # w~wersji tagsetu z~2009/06/23 nie ma aliasów i~nie są one używane w~Anotatorni.
                # (są natomiast używane części mowy, przechowywane w~tabeli \tabela{cz_m}, powiązanej z~tabelą \tabela{klasa_gram} i~używanej na poziomie \ac{WSD}.
                right = Regexp.new( '(' + Regexp.escape( prawe ) + ')')
                prawe.split('|').each{ |klasa|
                  self.create(
                              :typ => 'counteralias',
                              :lewe => klasa,
                              :prawe => lewe)
                }
                
              when 'attr'
                prawes = prawe.split
                right = prawes
                prawes.each{ |attr|
                  self.create(
                              :typ => 'counterattr',
                              :lewe => attr,
                              :prawe => lewe)
                }
                
              when 'pos'
                right = prawe.to_s.split.collect{ |pos|
                  if pos =~ /^\[(.*)\]$/ :  [$1, :optional]
                  else [pos]
                  end
                }
              end
              
              tags = self.create(
                                 :typ => currtype,
                                 :lewe => lewe,
                                 :prawe => right
                                 )
            end # of #{when /a-z/}
          end # of #{case line}
        }}
      
      # dopiszemy #:colonate tym atrybutom, które mają wartości dwukropkowane, 2009/02/11 jest to atrybut #numper_impt, a~2009/03/04 — numberperson_impt.
      true
    end # of main condition
  end # of #{self.zainicjuj}
  
  zainicjuj
  
  ATTR2PL = Hash.new { |h, klucz|
    h[klucz] = [ klucz, :f ] }

  ATTR2PL.update( 
                 # w bierniku, bo „nie wygląda na poprawną •”
                 {      
                   'accentability' => ['akcentowość', :f],
                   'accommodability' => ['akomodacyjność', :f],
                   'agglutination' => ['aglutynacyjność', :f],
                   'aspect' => ['aspekt', :m],
                   'aspect_aglt' => ['aspekt przyczepnika', :m],
                   'aspect_bedzie' => ['aspekt »będzie«', :m],
                   'aspect_pact' => ['aspectum participii activi', :m],
                   'aspect_pant' => ['aspekt partycypu anteriorycznego', :m],
                   'aspect_pcon' => ['aspekt partycypla kontemporaryjnego', :m],
                   'case' => ['przypadek', :m],
                   'case_depr' => ['przypadek deprecjatywu', :m],
                   'case_prep' => ['przypadek prepozycjonału', :m],
                   'case_siebie' => ['przypadek »siebie«', :m],
                   'degree' => ['stopień', :m],
                   'gender' => ['rodzaj', :m],
                   'gender_depr' => ['rodzaj deprecjatywu', :m],
                   'gender_ger' => ['genum gerundii', :m],
                   'gender_numcol' => ['rodzaj numerału kolekcjonalnego', :m],
                   'negation' => ['przeczenie', :n],
                   'number' => ['liczbę', :f],
                   'number_depr' => ['liczbę deprecjatywu', :f],
                   'numberperson_impt' => ['liczboosobę rozkaźnika', :f],
                   'person' => ['osobę', :f],
                   'post-prepositionality' => ['poprzyimkowość', :f],
                   'vocalicity' =>['wokaliczność', :f]
                 }
                 )
  
  @@colonates = self.find( :all, :conditions =>
                           "typ='attr' and prawe like '%:%' " ).collect {|ts|
    ts.lewe }

  def self.colonates
    @@colonates
  end

  def self.colonate?( attr )
    @@colonates.include?( attr )
  end


  POPRAWNY = {
    :m => 'poprawny',
    :f => 'poprawną',
    :n => 'poprawne'}
  
  ATRYBUTU = Hash.new( 'atrybutów' )
  
  ATRYBUTU.merge!( 1 => 'atrybutu' )
  
  @@bez_igna = true
  # W skrypcie sprawdzającym tagi dla ŁD zmienimy wartość tej zmiennej.


  def self.znajdz( typ, lewe, like=nil )
    ign_clause = Proc.new { |cond|
      if  typ == 'pos' and @@bez_igna
        cond[ 0 ] += " and lewe<>'ign' "
        # klauzula wykluczająca ign spośród rozważanych tagów.
      end }
    unless like
      condits = [ " typ = ? ",  typ ]
      ign_clause.call( condits )
      if lewe == :all
        self.find( :all, :conditions => condits )
      else
        condits[0] += " and lewe = ? "
        condits[2] = lewe
        self.find(:first, :conditions => condits )
      end
    else # the #lewe argument is like
      condition = [" typ = ? and lewe like ?  escape '\\' ", typ, lewe ] 
      ign_clause.call( condition )
      self.find( :all, :conditions => condition )
    end # of #{unless like}
  end # of #{self.znajdz}
  
  definition="
  def self.znajdz_xxx( lewe, like=nil )
    self.znajdz( 'xxx', lewe, like )
  end
  
  def znajdz_xxx( lewe, like=nil )
    self.class.znajdz_xxx( lewe, like )
  end"

   # definiujemy metody klasy i egzemplarza znajdz_pos
  eval definition.gsub( 'xxx', 'pos' )

  # definiujemy metody klasy i egzemplarza znajdz_attr
  eval definition.gsub( 'xxx', 'attr' )

  # definiujemy metody klasy i egzemplarza znajdz_counterattr
  eval definition.gsub( 'xxx', 'counterattr' )

  def trash
  end

  @@preferred = Hash.new
  
  self.znajdz_attr( '%\_preferred', :like ).each{ |atr|
    alewe = atr.lewe
    a_s = alewe.split('_')
    @@preferred[[ a_s[0], a_s[1] ]] = alewe
  }

  def self.preferred?( atr, gc )
    @@preferred[ [atr, gc] ]
  end

  def self.preferreds
    @@preferred
  end

  def self.attr_rx( attr )
    za = znajdz_attr( attr )
    if za :    Regexp.new( '^(' + za.prawe.collect{ |pr| Regexp.escape( pr )}.join('|') +')$', 'i' )
    else nil
    end    
  end
  
  def attr_rx( attr )
    self.attr_rx( attr )
  end

  @@lemmatic = {
    'siebie' => Proc.new {|lemat|  
      if lemat=='siebie' : nil
      else  'jedynym dopuszczalnym lematem dla k.g. siebie jest »siebie«'
      end }
  }

  def self.check_tag( tag, lemat )
    # zwrócimy true lub [false, <komunikat co źle>, <tag z podkreśleniem na czerwono>]
    tag_split=tag.split(':')
    # znaleźć układ tagu dla klasy gramatycznej
    
    zwrotka = true
    komunikat = nil
    redtag = tag
    rozpoznanie = []
    
    kg_pos = znajdz_pos( tag_split[0] )
    
    if kg_pos
      kg = tag_split[0]
      atrybuty = kg_pos.prawe 
      atryb0, atryb1= atrybuty.dx, atrybuty.dy
      
      # zgodnie z radą MW sprawdzamy dla każdej pozycji uklad_tagu, czy odpowiednia pozycja tag_split daje się zinterpretować jako taki właśnie atrybut. 
      unless komunikat
        tag_split.delete_at(0)
        errmsg = []
        ile_opt = atryb1.compact.size
        tag_split.each_index{ |i|
          if self.colonate?(atryb0[i]) 
            # (2009/02/11) korzystamy z tego, że atrybuty dwukropkowe, mianowicie numberperson_impt, są wymagane.
            tag_split[i, 2]  = tag_split[ i, 2 ].join(':')
          end
          platr = ATTR2PL[atryb0[i]]
          poprawny_atryb = "#{POPRAWNY[platr[1]]} #{platr[0]}"
          niepoprawny = "poz. #{i+2} (»#{tag_split[i]}«) nie wygląda na #{poprawny_atryb}"
          nadmiarowy = "atrybut #{i+2} (#{tag_split[i]}) jest nadmiarowy"
          if atryb0[i]
            if tag_split[i] =~ attr_rx( atryb0[i] )
            else
              # korzystam z założenia, że atrybuty opcjonalne są na końcu
              if atryb1[i] == :optional #czyli mamy atrybut opcjonalny i człon tagu nie pasuje do niego. I dalej już są same opcjonalne. Korzystam też z tego, że nie ma więcej niż dwóch atrybutów opcjonalnych.
                # zakładam też, że opcjonalne, jeśli występują, to w takim porządku jak w tagsecie.
                dalej = true
                err_opt = []
                until (not atryb0[i]) or (not dalej)
                  attr_rx = znajdz_attr( atryb0[i] ).prawe
                  if tag_split[i] =~ attr_rx
                    dalej = false
                    err_opt = nil
                  else
                    err_opt << niepoprawny
                    atryb0.delete_at( i )
                    atryb1.delete_at( i )
                    ile_opt -= 1
                  end
                end
                errmsg += err_opt if err_opt
                
              else # atrybut obowiązkowy i niepoprawny
                errmsg << niepoprawny
              end
            end
          else
            errmsg << nadmiarowy
          end
        }
        
        brak_atr = atryb0.size - tag_split.size - ile_opt
        if brak_atr > 0
          ile_opc = atryb1.compact.size
          if ile_opc > 0 : brak_atryb = "od #{brak_atr-ile_opc} do #{brak_atr}"  #jeśli są opcjonalne we wzorcu
          else brak_atryb = brak_atr
          end
          errmsg << "brakuje #{brak_atryb} #{ATRYBUTU[brak_atr]}"
        end

        if pro = @@lemmatic[ kg ]
          if lemma_msg = pro.call( lemat )
            errmsg << lemma_msg 
          end
        end
        
        if errmsg[0]
          komunikat = errmsg.join(", ")
          zwrotka = false
        end
        
      end # of #{unless komunikat}
      
      return  [zwrotka, komunikat].compact
      
    else
      return [false, 'Nie rozpoznana klasa gramatyczna']
    end
    
  end# of #{self.check_tag}
  

def self.matches( parttag )
  pt = parttag.split(':')
  pos = znajdz_pos(pt[0])
  if pos
    pt0=pt[0]
    pt.delete_at( 0 )
    atrybuty = pos.prawe
    atryb0, atryb1= atrybuty.dx, atrybuty.dy
    atryb0.each_index{ |i|
      if self.colonate?( atryb0[i] )
        pt[i, 2] = pt[i,2].compact.join(':')
      end
      ##      puts [atryb0[i], pt0 ].inspect
      if ( not pt[i] ) and x = self.preferred?( atryb0[i], pt0 )
        atryb0[i] = x
      end
    }
    # atryb1 niesie informację o opcjonalności atrybutów.
    ile_opc = atryb1.nitems
    matches = [pt0]
    
    skojarz = Proc.new {|i| 
      # ta proc bierze dotychczasową tablicę częściowych tagów i do każdego jej elementu dokłada (domnaża kartezjańsko) wszystkie możliwe wartości atrybutu i-tego, po czym wynik takiej operacji kompatkuje i spłaszcza.
      znajdz_attr( atryb0[i] ).prawe.collect{ |a|
        if a =~ Regexp.new(
                           "^" + Regexp.escape( pt[i].to_s ) 
                           # #{, "i"} case-insetivity removed while fixing \#356 2010/6/16.
                           ) # warunek spełniony także, gdy atrybut jest pusty (#{nil}).
          matches.collect{ |m| m + ':' + a } 
        end
                           
      }.compact.flatten}
    
    0.upto([pt.size, atryb0.size - ile_opc].min) {|i|
      # mnożymy kartezjańsko zbiory dopasowań kolejnych atrybutów
      if atryb0[i] and not atryb1[i]
        matches =  skojarz.call( i )
      end
    }
    
    ## puts atryb1.join(':')
    ## puts atryb1.size
    atryb1.size.times{ |i|
      # dla atrybutów opcjonalnych (których więcej niż 1, mianowicie 2, ma tylko jedna klasa gramatyczna, 
      if atryb1[i] and ( pt[i] or (i-ile_opc >= 0 and pt[i-1#ile_opc
                                                        ])) # jeśli mamy opcjonalny lub następny jest opcjonalny — nie. Tylko jeśli mamy opcjonalny: 2008/9/23 AP stwierdził, że w tej jednej klasie gram. drugi arg. opcjonalny może się pojawić tylko gdy występuje pierwszy — język tagsetu jest za słaby, żeby to wyrazić. Na razie postanowił(liśmy) nie rozbudowywać języka tagsetu o zagnieżdżenia opcjonalności, tylko zakodować to na twardo.
        puts( i )
        newmatches =  skojarz.call(i)
        if pt[i] : matches = newmatches
        else  matches += newmatches
        end
      end}
    
  else # niepełna pos
    re = Regexp.new("^#{Regexp.escape( parttag )}" 
                    # #{, "i"} case-insensitivity removed 2010/6/16 while fixing \#356
                    )
    matches = znajdz_pos( :all ).collect{ |t|
      t.lewe if t.lewe.match re
    }.compact
  end
  
  return matches#  #.sort nie sortujemy, żeby np. przypadki były w kolejności przypadków, a nie alfabetycznej — będą tak, jak są w pliku.
end # of #{self.matches}



end# of class
