<?php

// must be run within Dokuwiki
if(!defined('DOKU_INC')) die();

define( 'WEIQI_ERROR_BAD_ATTRIBUTES',  0);
define( 'WEIQI_ERROR_ONE_CAPTION',     1);
define( 'WEIQI_ERROR_RECTANGULAR',     2);

class weiqi_parser {
    /**
     * Some config items in this array. Keys are explained below.
     * Provide the array in the constructor or the setter.
     *
     * attributes_key
     *     how to know a line is for the attributes
     * caption_key
     *     how to know a line is for the caption
     * img_path_web
     *     web img path (with trailing slash)
     * img_path_fs
     *     path to the img directory no the server for the parser to check
     *     if requested img files are installed (absolute or relative)
     * letter_sequence
     *     letter sequence (no 'i' on a goban)
     * plain_text_coeff
     *     when displaying plain text instead of images,
     *     if font-size:(image_size)px; it's too big, hence we have to reduce it
     * allowed_attribute_keys
     *     what can the user change in his/her code
     * default_attributes
     *     what attributes will be used if nothing is provided by the user
     *     here is the detail of the keys of this array:
     *           demo : boolean, whether to display the source for demo purpose
     *           goban : integer, number of the 'wood' file to use
     *           grid_size : integer (px), size of the square img files to use
     *           edges_width : integer (px)
     *           coords : boolean, whether to display the co-ordinates
     *           reverse_numbers : boolean
     *           reverse_letters : boolean
     *           start_number : integer
     *           start_letter : letter in the letter sequence
     *           advanced : boolean, whether to use advanced code
     */
    var $conf = array();
    /**
     * Setted to true if the parse() function has to complain.
     */
    var $error = false;
    /**
     * Among the constants defined above.
     */
    var $error_code = '';
    /**
     * Some errors need to pass some info about the error,
     * especially WEIQI_ERROR_BAD_ATTRIBUTES that fills this array with
     * array($key, $val) arrays when $key is not allowed ($val is '') or
     * when $key is allowed but not its $val value.
     */
    var $error_data = array();
    
    /**
     * Constructor, with a possible config array
     */
    function weiqi_parser($conf = array()) {
        $this->set_conf($conf);
    }
    
    /**
     * Setter for the conf array
     */
    function set_conf($conf) {
        $this->conf = $conf;
    }
    
    /**
     * Makes the parser to parse the code.
     * When no complaint is made ($this->error is then set to true),
     * this function returns the $goban_data array with:
     *      $attributes, $caption, $board, $width, $height, $source
     * for the render() function to work.
     * Returns nothing if an error has occured.
     */
    function parse($str) {
        $lines = explode("\n", $str);
        // init of attributes, caption and bi-dimensional array of chars
        $attributes_str = '';
        $caption = '';
        $board = array();
        
        // first pass for some checks, caption and attributes
        foreach ($lines as $line) {
            if ($line!='') {
                // check if this is the attributes line
                // maybe preg_match is better
                if (substr($line, 0, strlen($this->conf['attributes_key'])) == $this->conf['attributes_key']) {
                    $attributes_str.= ' '.substr($line, strlen($this->conf['attributes_key']));
                // check if this is the caption line
                } elseif (substr($line, 0, strlen($this->conf['caption_key'])) == $this->conf['caption_key']) {
                    if ($caption!='') {
                        $this->error = true;
                        $this->error_code = WEIQI_ERROR_ONE_CAPTION;
                        return;
                    }
                    $caption = trim(substr($line, strlen($this->conf['caption_key'])));
                } else {
                    // check length of our lines
                    // first store the one of the first line
                    if (empty($line_length)) $line_length = strlen($line);
                    if (strlen($line) != $line_length) {
                        $this->error = true;
                        $this->error_code = WEIQI_ERROR_RECTANGULAR;
                        return;
                    }
                }
            }
        }
        $attributes = $this->parse_goban_attributes($attributes_str);//echo 'att:<';print_r($attributes);echo '>';
        if ($this->error) return;
        
        // second pass for parsing the board code
        foreach ($lines as $line) {
            if ($line!='') {
                // check if this is the attributes or caption line
                $att = substr($line, 0, strlen($this->conf['attributes_key'])) == $this->conf['attributes_key'];
                $cap = substr($line, 0, strlen($this->conf['caption_key'])) == $this->conf['caption_key'];
                if ( !$att AND !$cap) {
                    $tmp_array_line = array();
                    // map the string to an array
                    // one by one for standard code,
                    // two by two for advanced code
                    if (!array_key_exists('advanced', $attributes)) {
                        for($i=0;$i<strlen($line);$i++)
                            $tmp_array_line[] = substr($line, $i, 1);
                    } else {
                        for($i=0;$i<strlen($line)/2;$i++)
                            $tmp_array_line[] = substr($line, 2*$i, 2);
                    }
                    // append this line to the main bi-dim array of chars
                    $board[] = $tmp_array_line;
                }
            }
        }
        $width = $attributes['advanced']?($line_length/2):$line_length;
        $height = count($board);
        return array($attributes, $caption, $board, $width, $height, $str);
    }
    
    /**
     * Returns the html table.
     * this function needs an array with these informations
     *     $attributes, $caption, $board, $width, $height, $source
     */
    function render($goban_data) {
        list($attributes, $caption, $board, $width, $height, $source) = $goban_data;
        // import default attributes
        foreach ($this->conf['default_attributes'] as $key => $val) $$key = $val;
        // import user-defined attributes
        // quite dangerous, but controlled by $allowed_keys
        // in the parse_goban_attributes() function
        foreach ($attributes as $key => $val) $$key = $val;
        // some adjustments
        $goban = 'images/wood'.$goban;
        $w1 = $grid_size;
        $w2 = $edges_width;
        
        $wout = ''; // weiqi output, appended to $renderer->doc at the end
        
        // should we output the source too (demo purpose)
        if ($demo) {
            $wout.= '<pre class="w-source">';
            $wout.= $source;
            $wout.= '</pre>';
        }
        
        // hardcoded style (I mean not CSSed) to be able to change the goban color
        $wout.= '<table style="background-image: url(\''.$this->conf['img_path_web'].$goban.'.gif\');" border="0" cellspacing="0" cellpadding="0">';
        if ($caption!='') $wout .= '<caption>'.$caption.'</caption>';
        // top coordinates
        if ($coords) {
            $wout .= '<tr>';
            $wout .= '<td>'.$this->img('images/blank',' ',$w1,$w1).'</td>';// blank for left coordinates
            $wout .= '<td>'.$this->img('images/blank',' ',$w2,$w1).'</td>';// blank for left edge
            for($x=1;$x<=$width;$x++) {
                if ($reverse_letters) $row_letter = $this->letter($width - ($x - ($start_letter - 1)) + 1);
                else $row_letter = $this->letter($x + ($start_letter - 1));
                $wout .= '<td>'.$this->img($w1.'/c'.$row_letter, $row_letter).'</td>';// top coordinates
            }
            $wout .= '<td>'.$this->img('images/blank',' ',$w2,$w1).'</td>';// blank for right edge
            $wout .= '<td>'.$this->img('images/blank',' ',$w1,$w1).'</td>';// blank for right coordinates
        $wout .= '</tr>';
        }
        // top edge
        $wout .= '<tr>';
        if ($coords) $wout .= '<td>'.$this->img('images/blank',' ',$w1,$w2).'</td>';// blank for left coordinates
        $wout .= '<td>'.$this->img($goban.'_ul', ' ').'</td>';// top left goban corner
        $wout .= '<td colspan="'.$width.'">'.$this->img($goban.'_u', ' ', $w1*$width, $w2).'</td>';// top corner
        $wout .= '<td>'.$this->img($goban.'_ur', ' ').'</td>';// top right goban corner
        if ($coords) $wout .= '<td>'.$this->img('images/blank',' ',$w1,$w2).'</td>';// blank for right coordinates
        $wout .= '</tr>';
        // body
        $y=0;
        foreach ($board as $line) {
            $y++;
            $x=0;
            $wout .= '<tr>';
            if ($reverse_numbers) $line_number = $y + ($start_number - 1);
            else $line_number = $height - $y + 1 + ($start_number - 1);
            
            if ($coords) $wout .= '<td>'.$this->img($w1.'/c'.$line_number, $line_number).'</td>';// left coordinates
            if ($y==1) $wout .= '<td rowspan="'.$height.'">'.$this->img($goban.'_l', ' ', $w2, $w1*$height).'</td>';// left edge
            // board
            foreach ($line as $char) {
                $x++;
                $wout .= '<td>'.$this->img($w1.'/'.$this->weiqi_code($char, $x, $y, $width, $height), $char, $w1).'</td>';
            }
            if ($y==1) $wout .= '<td rowspan="'.$height.'">'.$this->img($goban.'_r', ' ', $w2, $w1*$height).'</td>';// right edge
            if ($coords) $wout .= '<td>'.$this->img($w1.'/c'.$line_number, $line_number).'</td>';// left coordinates
            $wout .= '</tr>';
        }
        // bottom edge
        $wout .= '<tr>';
        if ($coords) $wout .= '<td>'.$this->img('images/blank',' ',$w1,$w2).'</td>';// blank for left coordinates
        $wout .= '<td>'.$this->img($goban.'_dl', ' ').'</td>';// bottom left goban corner
        $wout .= '<td colspan="'.$width.'">'.$this->img($goban.'_d', ' ', $w1*$width, $w2).'</td>';
        $wout .= '<td>'.$this->img($goban.'_dr', ' ').'</td>';// bottom right goban corner
        if ($coords) $wout .= '<td>'.$this->img('images/blank',' ',$w1,$w2).'</td>';// blank for right coordinates
        $wout .= '</tr>';
        // bottom coordinates
        if ($coords) {
            $wout .= '<tr>';
            $wout .= '<td>'.$this->img('images/blank',' ',$w1,$w1).'</td>';// blank for left coordinates
            $wout .= '<td>'.$this->img('images/blank',' ',$w2,$w1).'</td>';// blank for left edge
            for($x=1;$x<=$width;$x++) {
                if ($reverse_letters) $row_letter = $this->letter($width - ($x - ($start_letter - 1)) + 1);
                else $row_letter = $this->letter($x + ($start_letter - 1));
                $wout .= '<td>'.$this->img($w1.'/c'.$row_letter, $row_letter).'</td>';// bottom coordinates
            }
            $wout .= '<td>'.$this->img('images/blank',' ',$w2,$w1).'</td>';// blank for right edge
            $wout .= '<td>'.$this->img('images/blank',' ',$w1,$w1).'</td>';// blank for right coordinates
            $wout .= '</tr>';
        }
        $wout.= '</table>';
        return $wout;
    }
    /**
     * A character (or two in advanced mode) is translated to
     * the relevant image file for the img() function to work.
     * If ends with a #, img() will display text.
     */
    function weiqi_code($str, $x, $y, $width, $height) {
        $str = trim($str);
        if (strlen($str) == 1) {
            // normal code
            switch ($str) {
                // goban
                case '.': return 'e';
                case ',': return 'h';
                case '+':
                    if ( $x==$width  && $y==1        ) return 'ur';
                    if ( $x==1       && $y==1        ) return 'ul';
                    if ( $x==1       && $y==$height  ) return 'dl';
                    if ( $x==$width  && $y==$height  ) return 'dr';
                case '-':
                    if ( $y==1        ) return 'u';
                    if ( $y==$height  ) return 'd';
                case '|':
                    if ( $x==1       ) return 'el';
                    if ( $x==$width  ) return 'er';
                
                // stones
                case 'x': return 'b';
                case 'X': return 'bm';
                case 'o': return 'w';
                case 'O': return 'wm';
                
                // for letters or numbers, we append an '#'
                // for them to appear as plain text
                
                // letters and numbers
                case '1': return '1#';
                case '2': return '2#';
                case '3': return '3#';
                case '4': return '4#';
                case '5': return '5#';
                case '6': return '6#';
                case '7': return '7#';
                case '8': return '8#';
                case '9': return '9#';
                    
                case 'a': return 'a#';
                case 'b': return 'b#';
                case 'c': return 'c#';
                case 'd': return 'd#';
                case 'e': return 'e#';
                case 'f': return 'f#';
                case 'g': return 'g#';
                case 'h': return 'h#';
                case 'i': return 'i#';
                case 'j': return 'j#';
                case 'k': return 'k#';
                case 'l': return 'l#';
                case 'm': return 'm#';
                case 'n': return 'n#';
                case 'p': return 'p#';
                case 'q': return 'q#';
                case 'r': return 'r#';
                case 's': return 's#';
                case 't': return 't#';
                case 'u': return 'u#';
                case 'v': return 'v#';
                case 'w': return 'w#';
                case 'y': return 'y#';
                case 'z': return 'z#';
                
                case 'A': return 'A#';
                case 'B': return 'B#';
                case 'C': return 'C#';
                case 'D': return 'D#';
                case 'E': return 'E#';
                case 'F': return 'F#';
                case 'G': return 'G#';
                case 'H': return 'H#';
                case 'I': return 'I#';
                case 'J': return 'J#';
                case 'K': return 'K#';
                case 'L': return 'L#';
                case 'M': return 'M#';
                case 'N': return 'N#';
                case 'P': return 'P#';
                case 'Q': return 'Q#';
                case 'R': return 'R#';
                case 'S': return 'S#';
                case 'T': return 'T#';
                case 'U': return 'U#';
                case 'V': return 'V#';
                case 'W': return 'W#';
                case 'Y': return 'Y#';
                case 'Z': return 'Z#';
            }
        } else {
            // advanced code
            // for letters or numbers, we append an '#'
            // for them to appear as plain text
            
            // the rest of the numbers
            if (is_numeric($str)) return $str.'#';
            else {
            // the rest of the available symbols
                $ch1 = substr($str, 0, 1);
                $ch2 = substr($str, 1, 1);
                // first, the rest of the letters
                if ($ch2 == 'l') return $ch1.'#';
                // now the goban marks and stones
                // ok it's a simple map, but DGS files for red squares have a d
                $ch2 = ($ch2=='r')?'d':$ch2;
                
                // some checks now
                // if nothing found, let's return nothing
                // so the img() function can report a mistake
                $available_second_chars = 'cstbwxdg1234567890';
                if (strpos($available_second_chars, $ch2) === false) return '';
                // no black stone with a black square, same for white
                if ($ch1.$ch2=='bb' OR $ch1.$ch2=='ww') return '';
                
                switch ($ch1) {
                    // goban and marks
                    case '.': return 'e'.$ch2;
                    case ',': return 'h'.$ch2;
                    case '+':
                        if ( $x==$width  && $y==1        ) return 'ur'.$ch2;
                        if ( $x==1       && $y==1        ) return 'ul'.$ch2;
                        if ( $x==1       && $y==$height  ) return 'dl'.$ch2;
                        if ( $x==$width  && $y==$height  ) return 'dr'.$ch2;
                    case '-':
                        if ( $y==1        ) return 'u'.$ch2;
                        if ( $y==$height  ) return 'd'.$ch2;
                    case '|':
                        if ( $x==1       ) return 'el'.$ch2;
                        if ( $x==$width  ) return 'er'.$ch2;
                    
                    // stones
                    case 'x': ;
                    case 'X': return 'b'.($ch2=='0'?'10':$ch2);
                    case 'o': ;
                    case 'O': return 'w'.($ch2=='0'?'10':$ch2);
                }
            }
        }
        // if nothing found, let's return nothing,
        // so the img() function can report a mistake
        return '';
    }
    
    /**
     * Number to letter using $this->conf['letter_sequence'])
     */
    function letter($n) {
        $n--;
        if ($n >= 0 AND $n < strlen($this->conf['letter_sequence']))
            return substr($this->conf['letter_sequence'], $n, 1);
        // this can help debugging the wiki code
        else return $n;
    }
    
    /**
     * Returns an img tag or text to fille the table.
     */
    function img($src, $alt=false, $w=false, $h=false) {
        $text_px = floor($this->conf['plain_text_coeff']*$w);
        // '/' at the end means we have to report a mistake
        if (preg_match('@/$@', $src))
            return '<span class="w-report" style="font-size:'.$text_px.'px">@</span>';
        // '#' at the end means we have a number or a letter
        // numbers and letters are displayed with simple text
        if (preg_match('@/(.+)#$@', $src, $match))
            return '<span style="font-size:'.$text_px.'px">'.$match[1].'</span>';
        
        // the rest is done with the usual img html tag
        $html = '';
        $html.= '<img';
        if($w) $html.= ' width="'.$w.'"';
        if($h) $html.= ' height="'.$h.'"';
        $html.= ' src="'.$this->conf['img_path_web'].$src.'.gif"';
        if($alt) $html.= ' alt="'.$alt.'"';
        $html.= ' />';
        return $html;
    }
    
    /**
     * Parses the attribute lines.
     * Returns an array that will be used by the render() function if everything
     * is correct.
     * Returns nothing and sets the error vars correctly if not.
     */
    function parse_goban_attributes($str) {
        $str = trim($str);
        if ($str=='') return array();
        
        $ret = array();
        $attributes = explode(' ', $str);
        $errors = array();
        foreach ($attributes as $attribute) {
            list($key,$value) = preg_split('/=/',$attribute,2);
            
            // report non allowed keys
            if (!in_array($key, $this->conf['allowed_attribute_keys']))
                    $errors[$key] = '';
            
            if ($key == 'demo') $ret[$key] = true;
            
            if ($key == 'goban') {
                if (!is_file(realpath($this->conf['img_path_fs'].'images/wood'.$value.'.gif'))) {
                    $errors[$key] = $value;
                }
                $ret['goban'] = $value;
            }
            
            if ($key == 'coords') $ret[$key] = true;
            
            if ($key == 'grid_size') {
                if (!is_dir(realpath($this->conf['img_path_fs'].$value))) {
                    $errors[$key] = $value;
                }
                $ret['grid_size'] = $value;
            }
            
            if ($key == 'reverse_numbers') $ret[$key] = true;
            if ($key == 'reverse_letters') $ret[$key] = true;
            
            if ($key == 'start_number') {
                if ($value<1 OR $value>25) {
                    $errors[$key] = $value;
                }
                $ret['start_number'] = $value;
            }
            if ($key == 'start_letter') {
                $strpos = strpos($this->conf['letter_sequence'], strtolower($value));
                if ($strpos===false) {
                    $errors[$key] = $value;
                }
                $ret['start_letter'] = $strpos + 1;
            }
            
            if ($key == 'advanced') $ret[$key] = true;
        }
        if (!empty($errors)) {
            $this->error = true;
            $this->error_code = WEIQI_ERROR_BAD_ATTRIBUTES;
            $this->error_data = array('attributes_str' => $str, 'errors' => $errors);
            return;
        }
        return $ret;
    }
}
