# 이 프로그램은 크리에이티브 커먼즈 저작자표시-비영리-동일조건변경허락 3.0 Unported 라이선스에 따라 이용할 수 있습니다.
require 'net/https'
require 'nokogiri'
require 'open-uri'

# Povis class
# author: gwangyi(gwangyi@postech.ac.kr)
# 포비스 로그인과 몇 가지 기능을 자동화시켜주어 웹 브라우저 없이 수행할 수 있도록 한 라이브러리 모음
class Povis
  # initialize
  # parameters
  #  - id: 포비스 아이디
  #  - pass: 포비스 비번
  # 아이디와 비번을 받고, 쿠키의 해시를 초기화시켜 작업을 시작할 수 있도록 준비한다.
  def initialize(id, pass)
    @id = id;
    @pass = pass;
    @cookies = Hash.new;
  end

  # start
  # parameters
  #  - id: 포비스 아이디
  #  - pass: 포비스 비번
  # 아이디와 비번을 받아 Povis 객체를 만들어 블록에 전달한다.
  def Povis.start(id, pass)
    povis = Povis.new(id, pass)
    povis.start
    yield(povis)
  end

  # start
  # 로그인하고 쿠키를 보관하여 작업을 시작할 준비에 들어간다.
  def start
    header = Hash.new
    header['User-Agent'] = "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; SLCC1; .NET CLR 2.0.50727; .NET CLR 3.0.04506; .NET CLR 1.1.4322)"
    header['Accept-Language'] = "ko"
    site = Net::HTTP.new("povis.postech.ac.kr", 443)
    site.use_ssl = true
    site.verify_mode = OpenSSL::SSL::VERIFY_NONE
    # HTTPS를 이용한 로그인
    response = site.post("/irj/portal", "login_submit=on&login_do_redirect=1&no_cert_storing=on&j_user=#{@id}&j_password=#{@pass}", header)
    update_cookies(response)

    header = Hash.new
    header['User-Agent'] = "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; SLCC1; .NET CLR 2.0.50727; .NET CLR 3.0.04506; .NET CLR 1.1.4322)"
    header['Accept-Language'] = "ko"
    site = Net::HTTP.new("gwap.postech.ac.kr", 80)
    add_cookies(header)
    # 로그인 후에 sso에 로그인
    response = site.post("/sso/sso_login.jsp", nil, header)
    update_cookies(response)

    add_cookies header
    # 서블렛 작업을 위해서는 이 것이 더 필요하다
    response = site.post '/servlet/com.nanum.xf.servlet.orglinkage.XFOrgLinkageServlet', "protID=RpcNFOrgLinkageLoginCookie&personID=#{@id}", header
    update_cookies response

    nil
  end

  # sikdan
  # return
  #  :wisdom => 위즈덤 식단 표, :freedom => 프리덤 식단 표 로 이루어진 해시
  # 가장 최근에 올라온 식단 정보를 입수하여 파싱, table 형태로 반환한다.
  # 표의 형태는 다음과 같다.
  #  프리덤(학식)
  #     0    1     2     3     4     5     6     7     8
  #   날짜 아침B 아침C 점심A 점심B 저녁A 저녁B 점심C 저녁C
  #  위즈덤
  #     0    1    2        3
  #   날짜  정식 일품 프리덤C 중식
  def sikdan
    site = Net::HTTP.new 'gwap.postech.ac.kr'
    header = { 'User-Agent' => 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; SLCC1; .NET CLR 2.0.50727; .NET CLR 3.0.04506; .NET CLR 1.1.4322)',
               'Accept-Language' => 'ko',
               'Accept' => '*/*' }

    add_cookies header
    # 게시판 목록 요청. 134번이 식단 게시판이다
    response = site.post '/servlet/com.nanum.xf.servlet.board.XFBoardServlet', 'protID=RpcNFBoardGetReadListRandom&boardId=134&page=1', header
    update_cookies response

    # 게시물 목록은 //ndata/info 안에 들어있다
    doc = Nokogiri::XML(response.body)
    doc = Nokogiri::XML(doc.xpath('//ndata/info').inner_text.gsub(/\A\s+|\s+\Z/n,''))
    # 각각의 게시판의 레코드는 //root/recordlist/record 에 들어있다
    records = doc.xpath '//root/recordlist/record'

    wisdom = nil
    freedom = nil

    for record in records
      title = record.xpath('field[4]').inner_text.gsub(/\s/, '') # 제목을 (4번째)뽑아내서
      if title.match(/위즈덤|wisdom/) # 위즈덤이 들어있는 것과
        wisdom = Nokogiri::HTML(open(URI.join('http://gwap.postech.ac.kr/', record.xpath('userdata/contentUrl').inner_text).to_s))
      elsif title.match(/프리덤|freedom/) # 프리덤이 들어 있는 것을 골라낸다
        freedom = Nokogiri::HTML(open(URI.join('http://gwap.postech.ac.kr/', record.xpath('userdata/contentUrl').inner_text).to_s))
      end
    end

    wisdom_tbl = parse_table(wisdom.css('table')[0]) # 각각에 있는 표를 분석해서 받아낸다
    freedom_tbl = parse_table(freedom.css('table')[0])

    i = 1
    freedom = []
    while i < freedom_tbl.length
      freedom.push freedom_tbl[i]
      freedom[-1][0] = freedom[-1][0].join.gsub /^.*([0-9]{2}).([0-9]{2}).*$/, '\1/\2'
      i = i + 2
    end # 맨 처음 행의 설명과 칼로리들을 제거

    i = 1
    wisdom = []
    while i < wisdom_tbl.length
      wisdom.push wisdom_tbl[i]
      wisdom[-1][0] = wisdom[-1][0].join.gsub /^.*([0-9]{2}).([0-9]{2}).*$/, '\1/\2'
      i = i + 2
    end

    { :wisdom => wisdom, :freedom => freedom }
  end

  private
  # 테이블을 파싱해서 배열로 만든다
  # rowspan, colspan을 고려해서 span된 셀의 경우 같은 내용을 복사해서 넣는다.
  # 가장 왼쪽 행의 셀을 기준으로 세로 행을 계산해 연결하여 붙인다.
  # 예를 들어
  #   | A
  #   +--
  # X | B
  #   +--
  #   | C
  # 의 표가 있을 경우
  # X | A, B, C 로 합쳐진다.
  # 그리고 식단표에 맞추어진 기능으로,
  # p 태그 안에 있는 내용만을 배열로 만들어 작업한다
  def parse_table(table)
    i = 0
    j = 0
    ret = []
    rows = table.xpath('tr')
    for row in rows
      j = 0

      for cell in row.xpath('td')
        ret[i] = [] if ret[i].nil?
        j = j + 1 while ret[i][j]
        colspan = cell.attribute('colspan')
        rowspan = cell.attribute('rowspan')
        colspan = colspan ? colspan.value.to_i : 1
        rowspan = rowspan ? rowspan.value.to_i : 1
        for j2 in j...(j + colspan)
          ret[i][j2] = cell.xpath('p').map{|p|p.inner_text}
        end
        for i2 in (i + 1)...(i + rowspan)
          ret[i2] = [] if ret[i2].nil?
          for j2 in j...(j + colspan)
            ret[i2][j2] = []
          end
        end
        j = j + colspan
      end
      i = i + 1
    end

    i = 0
    while i < ret.length
      first_cell = rows[i].xpath('td[1]')[0]
      first_rowspan = first_cell.attribute('rowspan')
      first_rowspan = first_rowspan ? first_rowspan.value.to_i : 1
      for i2 in (i + 1)...(i + first_rowspan)
        for j in 0...ret[i2].length
          ret[i][j].concat ret[i2][j]
        end
        ret[i2] = nil
      end
      i = i + first_rowspan
    end
    ret.delete_if{|x| x.nil?}
    ret.each {|r| r.delete_if{|c| c.nil? or c.length == 0 } }
  end

  def add_cookies(header)
    retval = String.new
    @cookies.each { |k, v| retval << "#{k}=#{v}; " }
    header['Cookie'] = retval
  end

  def update_cookies(res)
    cookies = res['set-cookie']
    if cookies.nil?
      #@cookies = Hash.new
    else
      cookie_items = cookies.split(',')
      cookie_items.each do |cookie|
        cookie = cookie.sub(/;.*$/, '').gsub(/^\s+|\s+$/, '')
        pair = cookie.split('=')
        pair = { "#{pair[0]}" => "#{pair[1]}" }
        @cookies.update(pair)
      end
    end
  end

end

