<?php
/**
 * Html2Pdf Library - parsing Css class
 *
 * HTML => PDF converter
 * distributed under the OSL-3.0 License
 *
 * @package   Html2pdf
 * @author    Laurent MINGUET <webmaster@html2pdf.fr>
 * @copyright 2025 Laurent MINGUET
 */

namespace Spipu\Html2Pdf\Parsing;

use Spipu\Html2Pdf\CssConverter;
use Spipu\Html2Pdf\MyPdf;
use Spipu\Html2Pdf\Security\SecurityInterface;

class Css
{
    /**
     * @var TagParser
     */
    protected $tagParser;

    /**
     * @var CssConverter
     */
    protected $cssConverter;

    /**
     * @var SecurityInterface
     */
    private $security;

    /**
     * @var MyPdf
     */
    protected $pdf         = null;

    protected $onlyLeft    = false; // flag if we are in a sub html => only "text-align:left" is used
    protected $defaultFont = null;  // default font to use if the asked font does not exist

    public $value        = array(); // current values
    public $css          = array(); // css values
    public $cssKeys      = array(); // css key, for the execution order
    public $table        = array(); // level history

    /**
     * @param MyPdf $pdf
     * @param TagParser $tagParser
     * @param CssConverter $cssConverter
     * @param SecurityInterface $security
     */
    public function __construct(
        MyPdf $pdf,
        TagParser $tagParser,
        CssConverter $cssConverter,
        SecurityInterface $security
    ) {
        $this->setSecurityService($security);
        $this->cssConverter = $cssConverter;
        $this->init();
        $this->setPdfParent($pdf);
        $this->tagParser = $tagParser;
    }

    public function setSecurityService(SecurityInterface $security): self
    {
        $this->security = $security;
        return $this;
    }

    /**
     * Set the $pdf parent object
     *
     * @param  MyPdf &$pdf reference to the Html2Pdf parent
     *
     * @return void
     */
    public function setPdfParent(&$pdf)
    {
        $this->pdf = &$pdf;
    }

    /**
     * Inform that we want only "test-align:left" because we are in a sub HTML
     *
     * @return void
     */
    public function setOnlyLeft()
    {
        $this->value['text-align'] = 'left';
        $this->onlyLeft = true;
    }

    /**
     * Get the vales of the parent, if exist
     *
     * @return array CSS values
     */
    public function getOldValues()
    {
        return isset($this->table[count($this->table)-1]) ? $this->table[count($this->table)-1] : $this->value;
    }

   /**
    * Define the Default Font to use, if the font does not exist, or if no font asked
    *
    * @param string $default default font-family. If null : Arial for no font asked, and error fot ont does not exist
    *
    * @return string  old default font-family
    */
    public function setDefaultFont($default = null)
    {
        $old = $this->defaultFont;
        $this->defaultFont = $default;
        if ($default) {
            $this->value['font-family'] = $default;
        }
        return $old;
    }

    /**
     * Init the object
     *
     * @return void
     */
    protected function init()
    {
        // init the Style
        $this->table = array();
        $this->value = array();
        $this->initStyle();

        // Init the styles without legacy
        $this->resetStyle();
    }

    /**
     * Init the CSS Style
     *
     * @return void
     */
    public function initStyle()
    {
        $this->value['id_tag']           = 'body';       // tag name
        $this->value['id_name']          = null;         // tag - attribute name
        $this->value['id_id']            = null;         // tag - attribute id
        $this->value['id_class']         = null;         // tag - attribute class
        $this->value['id_lst']           = array('*');   // tag - list of legacy
        $this->value['mini-size']        = 1.;           // specific size report for sup, sub
        $this->value['mini-decal']       = 0;            // specific position report for sup, sub
        $this->value['font-family']      = defined('PDF_FONT_NAME_MAIN') ? PDF_FONT_NAME_MAIN : 'Arial';
        $this->value['font-bold']        = false;
        $this->value['font-italic']      = false;
        $this->value['font-underline']   = false;
        $this->value['font-overline']    = false;
        $this->value['font-linethrough'] = false;
        $this->value['text-transform']   = 'none';
        $this->value['font-size']        = $this->cssConverter->convertFontSize('10pt');
        $this->value['text-indent']      = 0;
        $this->value['text-align']       = 'left';
        $this->value['vertical-align']   = 'middle';
        $this->value['line-height']      = 'normal';

        $this->value['position']         = null;
        $this->value['x']                = null;
        $this->value['y']                = null;
        $this->value['width']            = 0;
        $this->value['height']           = 0;
        $this->value['top']              = null;
        $this->value['right']            = null;
        $this->value['bottom']           = null;
        $this->value['left']             = null;
        $this->value['float']            = null;
        $this->value['display']          = null;
        $this->value['rotate']           = null;
        $this->value['overflow']         = 'visible';

        $this->value['color']            = array(0, 0, 0);
        $this->value['background']       = array(
            'color'    => null,
            'image'    => null,
            'position' => null,
            'repeat'   => null
        );
        $this->value['border']           = array();
        $this->value['padding']          = array();
        $this->value['margin']           = array();
        $this->value['margin-auto']      = false;

        $this->value['list-style-type']  = '';
        $this->value['list-style-image'] = '';

        $this->value['xc'] = null;
        $this->value['yc'] = null;

        $this->value['page-break-before'] = null;
        $this->value['page-break-after']  = null;
    }

    /**
     * Init the CSS Style without legacy
     *
     * @param string tag name
     *
     * @return void
     */
    public function resetStyle($tagName = '')
    {
        // prepare some values
        $border = $this->readBorder('solid 1px #000000');
        $units = array(
            '1px' => $this->cssConverter->convertToMM('1px'),
            '5px' => $this->cssConverter->convertToMM('5px'),
        );

        // prepare the Collapse attribute
        $collapse = isset($this->value['border']['collapse']) ? $this->value['border']['collapse'] : false;
        if (!in_array($tagName, array('tr', 'td', 'th', 'thead', 'tbody', 'tfoot'))) {
            $collapse = false;
        }

        // set the global css values
        $this->value['position']   = null;
        $this->value['x']          = null;
        $this->value['y']          = null;
        $this->value['width']      = 0;
        $this->value['height']     = 0;
        $this->value['top']        = null;
        $this->value['right']      = null;
        $this->value['bottom']     = null;
        $this->value['left']       = null;
        $this->value['float']      = null;
        $this->value['display']    = null;
        $this->value['rotate']     = null;
        $this->value['overflow']   = 'visible';
        $this->value['background'] = array('color' => null, 'image' => null, 'position' => null, 'repeat' => null);
        $this->value['border']     = array(
            't' => $this->readBorder('none'),
            'r' => $this->readBorder('none'),
            'b' => $this->readBorder('none'),
            'l' => $this->readBorder('none'),
            'radius' => array(
                'tl' => array(0, 0),
                'tr' => array(0, 0),
                'br' => array(0, 0),
                'bl' => array(0, 0)
            ),
            'collapse' => $collapse,
        );

        // specific values for some tags
        if (!in_array($tagName, array('h1', 'h2', 'h3', 'h4', 'h5', 'h6'))) {
            $this->value['margin'] = array('t'=>0,'r'=>0,'b'=>0,'l'=>0);
        }

        if (in_array($tagName, array('input', 'select', 'textarea'))) {
            $this->value['border']['t'] = null;
            $this->value['border']['r'] = null;
            $this->value['border']['b'] = null;
            $this->value['border']['l'] = null;
        }

        if ($tagName === 'p') {
            $this->value['margin']['t'] = null;
            $this->value['margin']['b'] = null;
        }
        if ($tagName === 'blockquote') {
            $this->value['margin']['t'] = 3;
            $this->value['margin']['r'] = 3;
            $this->value['margin']['b'] = 3;
            $this->value['margin']['l'] = 6;
        }
        $this->value['margin-auto'] = false;

        if (in_array($tagName, array('blockquote', 'div', 'fieldset'))) {
            $this->value['vertical-align'] = 'top';
        }

        if (in_array($tagName, array('fieldset', 'legend'))) {
            $this->value['border'] = array(
                't' => $border,
                'r' => $border,
                'b' => $border,
                'l' => $border,
                'radius' => array(
                    'tl' => array($units['5px'], $units['5px']),
                    'tr' => array($units['5px'], $units['5px']),
                    'br' => array($units['5px'], $units['5px']),
                    'bl' => array($units['5px'], $units['5px'])
                ),
                'collapse' => false,
            );
        }

        if (in_array($tagName, array('ul', 'li'))) {
            $this->value['list-style-type']  = '';
            $this->value['list-style-image'] = '';
        }

        if (!in_array($tagName, array('tr', 'td'))) {
            $this->value['padding'] = array(
                't' => 0,
                'r' => 0,
                'b' => 0,
                'l' => 0
            );
        } else {
            $this->value['padding'] = array(
                't' => $units['1px'],
                'r' => $units['1px'],
                'b' => $units['1px'],
                'l' => $units['1px']
            );
        }

        if ($tagName === 'hr') {
            $this->value['border'] = array(
                't' => $border,
                'r' => $border,
                'b' => $border,
                'l' => $border,
                'radius' => array(
                    'tl' => array(0, 0),
                    'tr' => array(0, 0),
                    'br' => array(0, 0),
                    'bl' => array(0, 0)
                ),
                'collapse' => false,
            );
            $this->cssConverter->convertBackground('#FFFFFF', $this->value['background']);
        }

        $this->value['xc'] = null;
        $this->value['yc'] = null;
    }

    /**
     * Init the PDF Font
     *
     * @return void
     */
    public function fontSet()
    {
        $family = strtolower($this->value['font-family']);

        $b = ($this->value['font-bold']        ? 'B' : '');
        $i = ($this->value['font-italic']      ? 'I' : '');
        $u = ($this->value['font-underline']   ? 'U' : '');
        $d = ($this->value['font-linethrough'] ? 'D' : '');
        $o = ($this->value['font-overline']    ? 'O' : '');

        // font style
        $style = $b.$i;

        if ($this->defaultFont) {
            if ($family === 'arial') {
                $family='helvetica';
            } elseif ($family === 'symbol' || $family === 'zapfdingbats') {
                $style='';
            }

            $fontkey = $family.$style;
            if (!$this->pdf->isLoadedFont($fontkey)) {
                $family = $this->defaultFont;
            }
        }

        if ($family === 'arial') {
            $family='helvetica';
        } elseif ($family === 'symbol' || $family === 'zapfdingbats') {
            $style='';
        }

        // complete style
        $style.= $u.$d.$o;

        // size : mm => pt
        $size = $this->value['font-size'];
        $size = 72 * $size / 25.4;

        // apply the font
        $this->pdf->SetFont($family, $style, $this->value['mini-size']*$size);
        $this->pdf->SetTextColorArray($this->value['color']);
        if ($this->value['background']['color']) {
            $this->pdf->SetFillColorArray($this->value['background']['color']);
        } else {
            $this->pdf->SetFillColor(255);
        }
    }

    /**
     * Add a level in the CSS history
     *
     * @return void
     */
    public function save()
    {
        array_push($this->table, $this->value);
    }

    /**
     * Remove a level in the CSS history
     *
     * @return void
     */
    public function load()
    {
        if (count($this->table)) {
            $this->value = array_pop($this->table);
        }
    }

    /**
     * Restore the Y position (used after a span)
     *
     * @return void
     */
    public function restorePosition()
    {
        if ($this->value['y'] == $this->pdf->GetY()) {
            $this->pdf->SetY($this->value['yc'], false);
        }
    }

    /**
     * Set the New position for the current Tag
     *
     * @return void
     */
    public function setPosition()
    {
        // get the current position
        $currentX = $this->pdf->GetX();
        $currentY = $this->pdf->GetY();

        // save it
        $this->value['xc'] = $currentX;
        $this->value['yc'] = $currentY;

        if ($this->value['position'] === 'relative' || $this->value['position'] === 'absolute') {
            if ($this->value['right'] !== null) {
                $x = $this->getLastWidth(true) - $this->value['right'] - $this->value['width'];
                if ($this->value['margin']['r']) {
                    $x-= $this->value['margin']['r'];
                }
            } else {
                $x = $this->value['left'];
                if ($this->value['margin']['l']) {
                    $x+= $this->value['margin']['l'];
                }
            }

            if ($this->value['bottom'] !== null) {
                $y = $this->getLastHeight(true) - $this->value['bottom'] - $this->value['height'];
                if ($this->value['margin']['b']) {
                    $y-= $this->value['margin']['b'];
                }
            } else {
                $y = $this->value['top'];
                if ($this->value['margin']['t']) {
                    $y+= $this->value['margin']['t'];
                }
            }

            if ($this->value['position'] === 'relative') {
                $this->value['x'] = $currentX + $x;
                $this->value['y'] = $currentY + $y;
            } else {
                $this->value['x'] = $this->getLastAbsoluteX()+$x;
                $this->value['y'] = $this->getLastAbsoluteY()+$y;
            }
        } else {
            $this->value['x'] = $currentX;
            $this->value['y'] = $currentY;
            if ($this->value['margin']['l']) {
                $this->value['x']+= $this->value['margin']['l'];
            }
            if ($this->value['margin']['t']) {
                $this->value['y']+= $this->value['margin']['t'];
            }
        }

        // save the new position
        $this->pdf->SetXY($this->value['x'], $this->value['y']);
    }

    /**
     * Analyse the CSS style to convert it into Form style
     *
     * @return array styles
     */
    public function getFormStyle()
    {
        $prop = array(
            'alignment' => $this->value['text-align']
        );

        if (isset($this->value['background']['color']) && is_array($this->value['background']['color'])) {
            $prop['fillColor'] = $this->value['background']['color'];
        }

        if (isset($this->value['border']['t']['color'])) {
            $prop['strokeColor'] = $this->value['border']['t']['color'];
        }

        if (isset($this->value['border']['t']['width'])) {
            $prop['lineWidth'] = $this->value['border']['t']['width'];
        }

        if (isset($this->value['border']['t']['type'])) {
            $prop['borderStyle'] = $this->value['border']['t']['type'];
        }

        if (!empty($this->value['color'])) {
            $prop['textColor'] = $this->value['color'];
        }

        if (!empty($this->value['font-size'])) {
            $prop['textSize'] = $this->value['font-size'];
        }

        return $prop;
    }

    /**
     * Analise the CSS style to convert it into SVG style
     *
     * @param string tag name
     * @param array  styles
     *
     * @return array svg style
     */
    public function getSvgStyle($tagName, &$param)
    {
        // prepare
        $tagName = strtolower($tagName);
        $id   = isset($param['id'])   ? strtolower(trim($param['id']))   : null;
        if (!$id) {
            $id   = null;
        }
        $name = isset($param['name']) ? strtolower(trim($param['name'])) : null;
        if (!$name) {
            $name = null;
        }

        // read the class attribute
        $class = array();
        $tmp = isset($param['class']) ? strtolower(trim($param['class'])) : '';
        $tmp = explode(' ', $tmp);
        foreach ($tmp as $v) {
            $v = trim($v);
            if ($v) {
                $class[] = $v;
            }
        }

        // identify the tag, and the direct styles
        $this->value['id_tag'] = $tagName;
        $this->value['id_name']   = $name;
        $this->value['id_id']     = $id;
        $this->value['id_class']  = $class;
        $this->value['id_lst']    = array();
        $this->value['id_lst'][] = '*';
        $this->value['id_lst'][] = $tagName;
        if (!isset($this->value['svg'])) {
            $this->value['svg'] = array(
                'stroke'         => null,
                'stroke-width'   => $this->cssConverter->convertToMM('1pt'),
                'fill'           => null,
                'fill-opacity'   => null,
            );
        }

        if (count($class)) {
            foreach ($class as $v) {
                $this->value['id_lst'][] = '*.'.$v;
                $this->value['id_lst'][] = '.'.$v;
                $this->value['id_lst'][] = $tagName.'.'.$v;
            }
        }
        if ($id) {
            $this->value['id_lst'][] = '*#'.$id;
            $this->value['id_lst'][] = '#'.$id;
            $this->value['id_lst'][] = $tagName.'#'.$id;
        }

        // CSS style
        $styles = $this->getFromCSS();

        // adding the style from the tag
        $styles = array_merge($styles, $param['style']);

        if (isset($styles['stroke'])) {
            $this->value['svg']['stroke']       = $this->cssConverter->convertToColor($styles['stroke'], $res);
        }
        if (isset($styles['stroke-width'])) {
            $this->value['svg']['stroke-width'] = $this->cssConverter->convertToMM($styles['stroke-width']);
        }
        if (isset($styles['fill'])) {
            $this->value['svg']['fill']         = $this->cssConverter->convertToColor($styles['fill'], $res);
        }
        if (isset($styles['fill-opacity'])) {
            $this->value['svg']['fill-opacity'] = 1.*$styles['fill-opacity'];
        }

        return $this->value['svg'];
    }

    /**
     * Analyse the CSS properties from the HTML parsing
     *
     * @param string $tagName
     * @param array  $param
     * @param array  $legacy
     *
     * @return boolean
     */
    public function analyse($tagName, &$param, $legacy = null)
    {
        // prepare the information
        $tagName = strtolower($tagName);
        $id   = isset($param['id'])   ? strtolower(trim($param['id']))    : null;
        if (!$id) {
            $id   = null;
        }
        $name = isset($param['name']) ? strtolower(trim($param['name']))  : null;
        if (!$name) {
            $name = null;
        }

        // get the class names to use
        $class = array();
        $tmp = isset($param['class']) ? strtolower(trim($param['class'])) : '';
        $tmp = explode(' ', $tmp);
        
        // replace some values
        $toReplace = array(
            '[[page_cu]]' => $this->pdf->getMyNumPage()
        );
        
        foreach ($tmp as $v) {
            $v = trim($v);
            if (strlen($v)>0) {
                $v = str_replace(array_keys($toReplace), array_values($toReplace), $v);
            }
            if ($v) {
                $class[] = $v;
            }
        }

        // prepare the values, and the list of css tags to identify
        $this->value['id_tag']   = $tagName;
        $this->value['id_name']  = $name;
        $this->value['id_id']    = $id;
        $this->value['id_class'] = $class;
        $this->value['id_lst']   = array();
        $this->value['id_lst'][] = '*';
        $this->value['id_lst'][] = $tagName;
        if (count($class)) {
            foreach ($class as $v) {
                $this->value['id_lst'][] = '*.'.$v;
                $this->value['id_lst'][] = '.'.$v;
                $this->value['id_lst'][] = $tagName.'.'.$v;
            }
        }
        if ($id) {
            $this->value['id_lst'][] = '*#'.$id;
            $this->value['id_lst'][] = '#'.$id;
            $this->value['id_lst'][] = $tagName.'#'.$id;
        }

        // get the css styles from class
        $styles = $this->getFromCSS();

        // merge with the css styles from tag
        $styles = array_merge($styles, $param['style']);
        if (isset($param['allwidth']) && !isset($styles['width'])) {
            $styles['width'] = '100%';
        }

        // reset some styles, depending on the tag name
        $this->resetStyle($tagName);

        // add the legacy values
        if ($legacy) {
            foreach ($legacy as $legacyName => $legacyValue) {
                if (is_array($legacyValue)) {
                    foreach ($legacyValue as $legacy2Name => $legacy2Value) {
                        $this->value[$legacyName][$legacy2Name] = $legacy2Value;
                    }
                } else {
                    $this->value[$legacyName] = $legacyValue;
                }
            }
        }

        // some flags
        $correctWidth = false;
        $noWidth = true;

        // read all the css styles
        foreach ($styles as $nom => $val) {
            switch ($nom) {
                case 'font-family':
                    $val = explode(',', $val);
                    $val = trim($val[0]);
                    $val = trim($val, '\'"');
                    if ($val && strtolower($val) !== 'inherit') {
                        $this->value['font-family'] = $val;
                    }
                    break;

                case 'font-weight':
                    $this->value['font-bold'] = ($val === 'bold');
                    break;

                case 'font-style':
                    $this->value['font-italic'] = ($val === 'italic');
                    break;

                case 'text-decoration':
                    $val = explode(' ', $val);
                    $this->value['font-underline']   = (in_array('underline', $val));
                    $this->value['font-overline']    = (in_array('overline', $val));
                    $this->value['font-linethrough'] = (in_array('line-through', $val));
                    break;

                case 'text-indent':
                    $this->value['text-indent'] = $this->cssConverter->convertToMM($val);
                    break;

                case 'text-transform':
                    if (!in_array($val, array('none', 'capitalize', 'uppercase', 'lowercase'))) {
                        $val = 'none';
                    }
                    $this->value['text-transform']  = $val;
                    break;

                case 'font-size':
                    $val = $this->cssConverter->convertFontSize($val, $this->value['font-size']);
                    if ($val) {
                        $this->value['font-size'] = $val;
                    }
                    break;

                case 'color':
                    $res = null;
                    $this->value['color'] = $this->cssConverter->convertToColor($val, $res);
                    if ($tagName === 'hr') {
                        $this->value['border']['l']['color'] = $this->value['color'];
                        $this->value['border']['t']['color'] = $this->value['color'];
                        $this->value['border']['r']['color'] = $this->value['color'];
                        $this->value['border']['b']['color'] = $this->value['color'];
                    }
                    break;

                case 'text-align':
                    $val = strtolower($val);
                    if (!in_array($val, array('left', 'right', 'center', 'justify', 'li_right'))) {
                        $val = 'left';
                    }
                    $this->value['text-align'] = $val;
                    break;

                case 'vertical-align':
                    $this->value['vertical-align'] = $val;
                    break;

                case 'width':
                    $this->value['width'] = $this->cssConverter->convertToMM($val, $this->getLastWidth());
                    if ($this->value['width'] && substr($val, -1) === '%') {
                        $correctWidth=true;
                    }
                    $noWidth = false;
                    break;

                case 'max-width':
                    $this->value[$nom] = $this->cssConverter->convertToMM($val, $this->getLastWidth());
                    break;

                case 'height':
                case 'max-height':
                    $this->value[$nom] = $this->cssConverter->convertToMM($val, $this->getLastHeight());
                    break;

                case 'line-height':
                    if (preg_match('/^[0-9\.]+$/isU', $val)) {
                        $val = floor($val*100).'%';
                    }
                    $this->value['line-height'] = $val;
                    break;

                case 'rotate':
                    if (!in_array($val, array(0, -90, 90, 180, 270, -180, -270))) {
                        $val = null;
                    }
                    if ($val<0) {
                        $val+= 360;
                    }
                    $this->value['rotate'] = $val;
                    break;

                case 'overflow':
                    if (!in_array($val, array('visible', 'hidden'))) {
                        $val = 'visible';
                    }
                    $this->value['overflow'] = $val;
                    break;

                case 'padding':
                    $val = explode(' ', $val);
                    foreach ($val as $k => $v) {
                        $v = trim($v);
                        if ($v !== '') {
                            $val[$k] = $v;
                        } else {
                            unset($val[$k]);
                        }
                    }
                    $val = array_values($val);
                    $this->duplicateBorder($val);
                    $this->value['padding']['t'] = $this->cssConverter->convertToMM($val[0], 0);
                    $this->value['padding']['r'] = $this->cssConverter->convertToMM($val[1], 0);
                    $this->value['padding']['b'] = $this->cssConverter->convertToMM($val[2], 0);
                    $this->value['padding']['l'] = $this->cssConverter->convertToMM($val[3], 0);
                    break;

                case 'padding-top':
                    $this->value['padding']['t'] = $this->cssConverter->convertToMM($val, 0);
                    break;

                case 'padding-right':
                    $this->value['padding']['r'] = $this->cssConverter->convertToMM($val, 0);
                    break;

                case 'padding-bottom':
                    $this->value['padding']['b'] = $this->cssConverter->convertToMM($val, 0);
                    break;

                case 'padding-left':
                    $this->value['padding']['l'] = $this->cssConverter->convertToMM($val, 0);
                    break;

                case 'margin':
                    if ($val === 'auto') {
                        $this->value['margin-auto'] = true;
                        break;
                    }
                    $val = explode(' ', $val);
                    foreach ($val as $k => $v) {
                        $v = trim($v);
                        if ($v !== '') {
                            $val[$k] = $v;
                        } else {
                            unset($val[$k]);
                        }
                    }
                    $val = array_values($val);
                    $this->duplicateBorder($val);
                    $this->value['margin']['t'] = $this->cssConverter->convertToMM($val[0], $this->getLastHeight());
                    $this->value['margin']['r'] = $this->cssConverter->convertToMM($val[1], $this->getLastWidth());
                    $this->value['margin']['b'] = $this->cssConverter->convertToMM($val[2], $this->getLastHeight());
                    $this->value['margin']['l'] = $this->cssConverter->convertToMM($val[3], $this->getLastWidth());
                    break;

                case 'margin-top':
                    $this->value['margin']['t'] = $this->cssConverter->convertToMM($val, $this->getLastHeight());
                    break;

                case 'margin-right':
                    $this->value['margin']['r'] = $this->cssConverter->convertToMM($val, $this->getLastWidth());
                    break;

                case 'margin-bottom':
                    $this->value['margin']['b'] = $this->cssConverter->convertToMM($val, $this->getLastHeight());
                    break;

                case 'margin-left':
                    $this->value['margin']['l'] = $this->cssConverter->convertToMM($val, $this->getLastWidth());
                    break;

                case 'border':
                    $val = $this->readBorder($val);
                    $this->value['border']['t'] = $val;
                    $this->value['border']['r'] = $val;
                    $this->value['border']['b'] = $val;
                    $this->value['border']['l'] = $val;
                    break;

                case 'border-style':
                    $val = explode(' ', $val);
                    foreach ($val as $valK => $valV) {
                        if (!in_array($valV, array('solid', 'dotted', 'dashed'))) {
                            $val[$valK] = null;
                        }
                    }
                    $this->duplicateBorder($val);
                    if ($val[0]) {
                        $this->value['border']['t']['type'] = $val[0];
                    }
                    if ($val[1]) {
                        $this->value['border']['r']['type'] = $val[1];
                    }
                    if ($val[2]) {
                        $this->value['border']['b']['type'] = $val[2];
                    }
                    if ($val[3]) {
                        $this->value['border']['l']['type'] = $val[3];
                    }
                    break;

                case 'border-top-style':
                    if (in_array($val, array('solid', 'dotted', 'dashed'))) {
                        $this->value['border']['t']['type'] = $val;
                    }
                    break;

                case 'border-right-style':
                    if (in_array($val, array('solid', 'dotted', 'dashed'))) {
                        $this->value['border']['r']['type'] = $val;
                    }
                    break;

                case 'border-bottom-style':
                    if (in_array($val, array('solid', 'dotted', 'dashed'))) {
                        $this->value['border']['b']['type'] = $val;
                    }
                    break;

                case 'border-left-style':
                    if (in_array($val, array('solid', 'dotted', 'dashed'))) {
                        $this->value['border']['l']['type'] = $val;
                    }
                    break;

                case 'border-color':
                    $res = false;
                    $val = preg_replace('/,[\s]+/', ',', $val);
                    $val = explode(' ', $val);
                    foreach ($val as $valK => $valV) {
                            $val[$valK] = $this->cssConverter->convertToColor($valV, $res);
                        if (!$res) {
                            $val[$valK] = null;
                        }
                    }
                    $this->duplicateBorder($val);
                    if (is_array($val[0])) {
                        $this->value['border']['t']['color'] = $val[0];
                    }
                    if (is_array($val[1])) {
                        $this->value['border']['r']['color'] = $val[1];
                    }
                    if (is_array($val[2])) {
                        $this->value['border']['b']['color'] = $val[2];
                    }
                    if (is_array($val[3])) {
                        $this->value['border']['l']['color'] = $val[3];
                    }

                    break;

                case 'border-top-color':
                    $res = false;
                    $val = $this->cssConverter->convertToColor($val, $res);
                    if ($res) {
                        $this->value['border']['t']['color'] = $val;
                    }
                    break;

                case 'border-right-color':
                    $res = false;
                    $val = $this->cssConverter->convertToColor($val, $res);
                    if ($res) {
                        $this->value['border']['r']['color'] = $val;
                    }
                    break;

                case 'border-bottom-color':
                    $res = false;
                    $val = $this->cssConverter->convertToColor($val, $res);
                    if ($res) {
                        $this->value['border']['b']['color'] = $val;
                    }
                    break;

                case 'border-left-color':
                    $res = false;
                    $val = $this->cssConverter->convertToColor($val, $res);
                    if ($res) {
                        $this->value['border']['l']['color'] = $val;
                    }
                    break;

                case 'border-width':
                    $val = explode(' ', $val);
                    foreach ($val as $valK => $valV) {
                            $val[$valK] = $this->cssConverter->convertToMM($valV, 0);
                    }
                    $this->duplicateBorder($val);
                    if ($val[0]) {
                        $this->value['border']['t']['width'] = $val[0];
                    }
                    if ($val[1]) {
                        $this->value['border']['r']['width'] = $val[1];
                    }
                    if ($val[2]) {
                        $this->value['border']['b']['width'] = $val[2];
                    }
                    if ($val[3]) {
                        $this->value['border']['l']['width'] = $val[3];
                    }
                    break;

                case 'border-top-width':
                    $val = $this->cssConverter->convertToMM($val, 0);
                    if ($val) {
                        $this->value['border']['t']['width'] = $val;
                    }
                    break;

                case 'border-right-width':
                    $val = $this->cssConverter->convertToMM($val, 0);
                    if ($val) {
                        $this->value['border']['r']['width'] = $val;
                    }
                    break;

                case 'border-bottom-width':
                    $val = $this->cssConverter->convertToMM($val, 0);
                    if ($val) {
                        $this->value['border']['b']['width'] = $val;
                    }
                    break;

                case 'border-left-width':
                    $val = $this->cssConverter->convertToMM($val, 0);
                    if ($val) {
                        $this->value['border']['l']['width'] = $val;
                    }
                    break;

                case 'border-collapse':
                    if ($tagName === 'table') {
                        $this->value['border']['collapse'] = ($val === 'collapse');
                    }
                    break;

                case 'border-radius':
                    $val = explode('/', $val);
                    if (count($val)>2) {
                        break;
                    }
                    $valH = $this->cssConverter->convertToRadius(trim($val[0]));
                    if (count($valH)<1 || count($valH)>4) {
                        break;
                    }
                    if (!isset($valH[1])) {
                        $valH[1] = $valH[0];
                    }
                    if (!isset($valH[2])) {
                        $valH = array($valH[0], $valH[0], $valH[1], $valH[1]);
                    }
                    if (!isset($valH[3])) {
                        $valH[3] = $valH[1];
                    }
                    if (isset($val[1])) {
                        $valV = $this->cssConverter->convertToRadius(trim($val[1]));
                        if (count($valV)<1 || count($valV)>4) {
                            break;
                        }
                        if (!isset($valV[1])) {
                            $valV[1] = $valV[0];
                        }
                        if (!isset($valV[2])) {
                            $valV = array($valV[0], $valV[0], $valV[1], $valV[1]);
                        }
                        if (!isset($valV[3])) {
                            $valV[3] = $valV[1];
                        }
                    } else {
                        $valV = $valH;
                    }
                    $this->value['border']['radius'] = array(
                                'tl' => array($valH[0], $valV[0]),
                                'tr' => array($valH[1], $valV[1]),
                                'br' => array($valH[2], $valV[2]),
                                'bl' => array($valH[3], $valV[3])
                            );
                    break;

                case 'border-top-left-radius':
                    $val = $this->cssConverter->convertToRadius($val);
                    if (count($val)<1 || count($val)>2) {
                        break;
                    }
                    $this->value['border']['radius']['tl'] = array($val[0], isset($val[1]) ? $val[1] : $val[0]);
                    break;

                case 'border-top-right-radius':
                    $val = $this->cssConverter->convertToRadius($val);
                    if (count($val)<1 || count($val)>2) {
                        break;
                    }
                    $this->value['border']['radius']['tr'] = array($val[0], isset($val[1]) ? $val[1] : $val[0]);
                    break;

                case 'border-bottom-right-radius':
                    $val = $this->cssConverter->convertToRadius($val);
                    if (count($val)<1 || count($val)>2) {
                        break;
                    }
                    $this->value['border']['radius']['br'] = array($val[0], isset($val[1]) ? $val[1] : $val[0]);
                    break;

                case 'border-bottom-left-radius':
                    $val = $this->cssConverter->convertToRadius($val);
                    if (count($val)<1 || count($val)>2) {
                        break;
                    }
                    $this->value['border']['radius']['bl'] = array($val[0], isset($val[1]) ? $val[1] : $val[0]);
                    break;

                case 'border-top':
                    $this->value['border']['t'] = $this->readBorder($val);
                    break;

                case 'border-right':
                    $this->value['border']['r'] = $this->readBorder($val);
                    break;

                case 'border-bottom':
                    $this->value['border']['b'] = $this->readBorder($val);
                    break;

                case 'border-left':
                    $this->value['border']['l'] = $this->readBorder($val);
                    break;

                case 'background-color':
                    $this->value['background']['color'] = $this->cssConverter->convertBackgroundColor($val);
                    break;

                case 'background-image':
                    $this->value['background']['image'] = $this->cssConverter->convertBackgroundImage($val);
                    break;

                case 'background-position':
                    $res = null;
                    $this->value['background']['position'] = $this->cssConverter->convertBackgroundPosition($val, $res);
                    break;

                case 'background-repeat':
                    $this->value['background']['repeat'] = $this->cssConverter->convertBackgroundRepeat($val);
                    break;

                case 'background':
                    $this->cssConverter->convertBackground($val, $this->value['background']);
                    break;

                case 'position':
                    if ($val === 'absolute') {
                        $this->value['position'] = 'absolute';
                    } elseif ($val === 'relative') {
                        $this->value['position'] = 'relative';
                    } else {
                        $this->value['position'] = null;
                    }
                    break;

                case 'float':
                    if ($val === 'left') {
                        $this->value['float'] = 'left';
                    } elseif ($val === 'right') {
                        $this->value['float'] = 'right';
                    } else {
                        $this->value['float'] = null;
                    }
                    break;

                case 'display':
                    if ($val === 'inline') {
                        $this->value['display'] = 'inline';
                    } elseif ($val === 'block') {
                        $this->value['display'] = 'block';
                    } elseif ($val === 'none') {
                        $this->value['display'] = 'none';
                    } else {
                        $this->value['display'] = null;
                    }
                    break;

                case 'top':
                case 'bottom':
                case 'left':
                case 'right':
                    $this->value[$nom] = $val;
                    break;

                case 'list-style':
                case 'list-style-type':
                case 'list-style-image':
                    if ($nom === 'list-style') {
                        $nom = 'list-style-type';
                    }
                    $this->value[$nom] = $val;
                    break;

                case 'page-break-before':
                case 'page-break-after':
                    $this->value[$nom] = $val;
                    break;

                case 'start':
                    $this->value[$nom] = intval($val);
                    break;

                default:
                    break;
            }
        }

        $return = true;

        // only for P tag
        if ($this->value['margin']['t'] === null) {
            $this->value['margin']['t'] = $this->value['font-size'];
        }
        if ($this->value['margin']['b'] === null) {
            $this->value['margin']['b'] = $this->value['font-size'];
        }

        // force the text align to left, if asked by html2pdf
        if ($this->onlyLeft) {
            $this->value['text-align'] = 'left';
        }

        // correction on the width (quick box)
        if ($noWidth
            && in_array($tagName, array('div', 'blockquote', 'fieldset'))
            && $this->value['position'] !== 'absolute'
        ) {
            $this->value['width'] = $this->getLastWidth();
            $this->value['width']-= $this->value['margin']['l'] + $this->value['margin']['r'];
        } else {
            if ($correctWidth) {
                if (!in_array($tagName, array('table', 'div', 'blockquote', 'fieldset', 'hr'))) {
                    $this->value['width']-= $this->value['padding']['l'] + $this->value['padding']['r'];
                    $this->value['width']-= $this->value['border']['l']['width'] + $this->value['border']['r']['width'];
                }
                if (in_array($tagName, array('th', 'td'))) {
                    $this->value['width']-= $this->cssConverter->convertToMM(isset($param['cellspacing']) ? $param['cellspacing'] : '2px');
                    $return = false;
                }
                if ($this->value['width']<0) {
                    $this->value['width']=0;
                }
            } else {
                if ($this->value['width']) {
                    if ($this->value['border']['l']['width']) {
                        $this->value['width'] += $this->value['border']['l']['width'];
                    }
                    if ($this->value['border']['r']['width']) {
                        $this->value['width'] += $this->value['border']['r']['width'];
                    }
                    if ($this->value['padding']['l']) {
                        $this->value['width'] += $this->value['padding']['l'];
                    }
                    if ($this->value['padding']['r']) {
                        $this->value['width'] += $this->value['padding']['r'];
                    }
                }
            }
        }
        if ($this->value['height']) {
            if ($this->value['border']['b']['width']) {
                $this->value['height'] += $this->value['border']['b']['width'];
            }
            if ($this->value['border']['t']['width']) {
                $this->value['height'] += $this->value['border']['t']['width'];
            }
            if ($this->value['padding']['b']) {
                $this->value['height'] += $this->value['padding']['b'];
            }
            if ($this->value['padding']['t']) {
                $this->value['height'] += $this->value['padding']['t'];
            }
        }

        if ($this->value['top'] != null) {
            $this->value['top']     = $this->cssConverter->convertToMM($this->value['top'], $this->getLastHeight(true));
        }
        if ($this->value['bottom'] != null) {
            $this->value['bottom']  = $this->cssConverter->convertToMM($this->value['bottom'], $this->getLastHeight(true));
        }
        if ($this->value['left'] != null) {
            $this->value['left']    = $this->cssConverter->convertToMM($this->value['left'], $this->getLastWidth(true));
        }
        if ($this->value['right'] != null) {
            $this->value['right']   = $this->cssConverter->convertToMM($this->value['right'], $this->getLastWidth(true));
        }

        if ($this->value['top'] && $this->value['bottom'] && $this->value['height']) {
            $this->value['bottom']  = null;
        }
        if ($this->value['left'] && $this->value['right'] && $this->value['width']) {
            $this->value['right']   = null;
        }

        return $return;
    }

    /**
     * Get the height of the current line
     *
     * @return float height in mm
     */
    public function getLineHeight()
    {
        $val = $this->value['line-height'];
        if ($val === 'normal') {
            $val = '108%';
        }
        return $this->cssConverter->convertToMM($val, $this->value['font-size']);
    }

    /**
     * Get the width of the parent
     *
     * @param  boolean $mode true => adding padding and border
     *
     * @return float width in mm
     */
    public function getLastWidth($mode = false)
    {
        for ($k=count($this->table)-1; $k>=0; $k--) {
            if ($this->table[$k]['width']) {
                $w = $this->table[$k]['width'];
                if ($mode) {
                    $w+= $this->table[$k]['border']['l']['width'] + $this->table[$k]['padding']['l'] + 0.02;
                    $w+= $this->table[$k]['border']['r']['width'] + $this->table[$k]['padding']['r'] + 0.02;
                }
                return $w;
            }
        }
        return $this->pdf->getW() - $this->pdf->getlMargin() - $this->pdf->getrMargin();
    }

    /**
     * Get the height of the parent
     *
     * @param  boolean $mode true => adding padding and border
     *
     * @return float height in mm
     */
    public function getLastHeight($mode = false)
    {
        for ($k=count($this->table)-1; $k>=0; $k--) {
            if ($this->table[$k]['height']) {
                $h = $this->table[$k]['height'];
                if ($mode) {
                    $h+= $this->table[$k]['border']['t']['width'] + $this->table[$k]['padding']['t'] + 0.02;
                    $h+= $this->table[$k]['border']['b']['width'] + $this->table[$k]['padding']['b'] + 0.02;
                }
                return $h;
            }
        }
        return $this->pdf->getH() - $this->pdf->gettMargin() - $this->pdf->getbMargin();
    }

    /**
     * Get the value of the float property
     *
     * @return string left/right
     */
    public function getFloat()
    {
        if ($this->value['float'] === 'left') {
            return 'left';
        }
        if ($this->value['float'] === 'right') {
            return 'right';
        }
        return null;
    }

    /**
     * Get the last value for a specific key
     *
     * @param  string $key
     *
     * @return mixed
     */
    public function getLastValue($key)
    {
        $nb = count($this->table);
        if ($nb>0) {
            return $this->table[$nb-1][$key];
        } else {
            return null;
        }
    }

    /**
     * Get the last absolute X
     *
     * @return float x
     */
    protected function getLastAbsoluteX()
    {
        for ($k=count($this->table)-1; $k>=0; $k--) {
            if ($this->table[$k]['x'] && $this->table[$k]['position']) {
                return $this->table[$k]['x'];
            }
        }
        return $this->pdf->getlMargin();
    }

    /**
     * Get the last absolute Y
     *
     * @return float y
     */
    protected function getLastAbsoluteY()
    {
        for ($k=count($this->table)-1; $k>=0; $k--) {
            if ($this->table[$k]['y'] && $this->table[$k]['position']) {
                return $this->table[$k]['y'];
            }
        }
        return $this->pdf->gettMargin();
    }

    /**
     * Get the CSS properties of the current tag
     *
     * @return array styles
     */
    protected function getFromCSS()
    {
        // styles to apply
        $styles = array();

        // list of the selectors to get in the CSS files
        $getit  = array();

        // get the list of the selectors of each tags
        $lst = array();
        $lst[] = $this->value['id_lst'];
        for ($i=count($this->table)-1; $i>=0; $i--) {
            $lst[] = $this->table[$i]['id_lst'];
        }

        // foreach selectors in the CSS files, verify if it match with the list of selectors
        foreach ($this->cssKeys as $key => $num) {
            if ($this->getReccursiveStyle($key, $lst)) {
                $getit[$key] = $num;
            }
        }

        // if we have selectors
        if (count($getit)) {
            // get them, but in the definition order, because of priority
            asort($getit);
            foreach ($getit as $key => $val) {
                $styles = array_merge($styles, $this->css[$key]);
            }
        }

        return $styles;
    }

    /**
     * Identify if the selector $key match with the list of tag selectors
     *
     * @param  string   $key CSS selector to analyse
     * @param  array    $lst list of the selectors of each tags
     * @param  string   $next next step of parsing the selector
     *
     * @return boolean
     */
    protected function getReccursiveStyle($key, $lst, $next = null)
    {
        // if next step
        if ($next !== null) {
            // we remove this step
            if ($next) {
                $key = trim(substr($key, 0, -strlen($next)));
            }
            array_shift($lst);

            // if no more step to identify => return false
            if (!count($lst)) {
                return false;
            }
        }

        // for each selector of the current step
        foreach ($lst[0] as $name) {
            // if selector = key => ok
            if ($key == $name) {
                return true;
            }

            // if the end of the key = the selector and the next step is ok => ok
            if (substr($key, -strlen(' '.$name)) === ' '.$name && $this->getReccursiveStyle($key, $lst, $name)) {
                return true;
            }
        }

        // if we are not in the first step, we analyse the sub steps (the pareng tag of the current tag)
        if ($next !== null && $this->getReccursiveStyle($key, $lst, '')) {
            return true;
        }

        // no corresponding found
        return false;
    }

    /**
     * Analyse a border
     *
     * @param   string $css css border properties
     *
     * @return  array border properties
     */
    public function readBorder($css)
    {
        // border none
        $none = array('type' => 'none', 'width' => 0, 'color' => array(0, 0, 0));

        // default value
        $type  = 'solid';
        $width = $this->cssConverter->convertToMM('1pt');
        $color = array(0, 0, 0);

        // clean up the values
        $css = explode(' ', $css);
        foreach ($css as $k => $v) {
            $v = trim($v);
            if ($v !== '') {
                $css[$k] = $v;
            } else {
                unset($css[$k]);
            }
        }
        $css = array_values($css);

        // read the values
        $res = null;
        foreach ($css as $value) {

            // if no border => return none
            if ($value === 'none' || $value === 'hidden') {
                return $none;
            }

            // try to convert the value as a distance
            $tmp = $this->cssConverter->convertToMM($value);

            // if the convert is ok => it is a width
            if ($tmp !== null) {
                $width = $tmp;
            // else, it could be the type
            } elseif (in_array($value, array('solid', 'dotted', 'dashed', 'double'))) {
                $type = $value;
            // else, it could be the color
            } else {
                $tmp = $this->cssConverter->convertToColor($value, $res);
                if ($res) {
                    $color = $tmp;
                }
            }
        }

        // if no witdh => return none
        if (!$width) {
            return $none;
        }

        // return the border properties
        return array('type' => $type, 'width' => $width, 'color' => $color);
    }

    /**
     * Duplicate the borders if needed
     *
     * @param  &array $val
     *
     * @return void
     */
    protected function duplicateBorder(&$val)
    {
        // 1 value => L => RTB
        if (count($val) == 1) {
            $val[1] = $val[0];
            $val[2] = $val[0];
            $val[3] = $val[0];
        // 2 values => L => R & T => B
        } elseif (count($val) == 2) {
            $val[2] = $val[0];
            $val[3] = $val[1];
        // 3 values => T => B
        } elseif (count($val) == 3) {
            $val[3] = $val[1];
        }
    }


    /**
     * Read a css content
     *
     * @param  &string $code
     *
     * @return void
     */
    protected function analyseStyle($code)
    {
        // clean the spaces
        $code = preg_replace('/[\s]+/', ' ', $code);

        // remove the comments
        $code = preg_replace('/\/\*.*?\*\//s', '', $code);

        // split each CSS code "selector { value }"
        preg_match_all('/([^{}]+){([^}]*)}/isU', $code, $match);

        // for each CSS code
        $amountMatch = count($match[0]);
        for ($k = 0; $k < $amountMatch; $k++) {

            // selectors
            $names = strtolower(trim($match[1][$k]));

            // css style
            $styles = trim($match[2][$k]);

            // explode each value
            $styles = explode(';', $styles);

            // parse each value
            $css = array();
            foreach ($styles as $style) {
                $tmp = explode(':', $style);
                if (count($tmp) > 1) {
                    $cod = $tmp[0];
                    unset($tmp[0]);
                    $tmp = implode(':', $tmp);
                    $css[trim(strtolower($cod))] = trim($tmp);
                }
            }

            // explode the names
            $names = explode(',', $names);

            // save the values for each names
            foreach ($names as $name) {
                // clean the name
                $name = trim($name);

                // if a selector with something like :hover => continue
                if (strpos($name, ':') !== false) {
                    continue;
                }

                // save the value
                if (!isset($this->css[$name])) {
                    $this->css[$name] = $css;
                } else {
                    $this->css[$name] = array_merge($this->css[$name], $css);
                }

            }
        }

        // get the list of the keys
        $this->cssKeys = array_flip(array_keys($this->css));
    }

    /**
     * Extract the css files from a html code
     *
     * @param  string $html
     *
     * @return string
     */
    public function extractStyle($html)
    {
        // the CSS content
        $style = ' ';

        // extract the link tags, and remove them in the html code
        preg_match_all('/<link([^>]*)>/isU', $html, $match);
        $html = preg_replace('/<link[^>]*>/isU', '', $html);
        $html = preg_replace('/<\/link[^>]*>/isU', '', $html);

        // analyse each link tag
        foreach ($match[1] as $code) {
            $tmp = $this->tagParser->extractTagAttributes($code);

            // if type text/css => we keep it
            if (isset($tmp['type']) && strtolower($tmp['type']) === 'text/css' && isset($tmp['href'])) {

                // get the href
                $url = (string) $tmp['href'];

                // get the content of the css file
                $this->security->checkValidPath($url);
                $content = @file_get_contents($url);

                // if "http://" in the url
                if (strpos($url, 'http://') !== false) {

                    // get the domain "http://xxx/"
                    $url = str_replace('http://', '', $url);
                    $url = explode('/', $url);
                    $urlMain = 'http://'.$url[0].'/';

                    // get the absolute url of the path
                    $urlSelf = $url;
                    unset($urlSelf[count($urlSelf)-1]);
                    $urlSelf = 'http://'.implode('/', $urlSelf).'/';

                    // adapt the url in the css content
                    $content = preg_replace('/url\(([^\\\\][^)]*)\)/isU', 'url('.$urlSelf.'$1)', $content);
                    $content = preg_replace('/url\((\\\\[^)]*)\)/isU', 'url('.$urlMain.'$1)', $content);
                } else {
                    // @TODO correction on url in absolute on a local css content
                    // $content = preg_replace('/url\(([^)]*)\)/isU', 'url('.dirname($url).'/$1)', $content);
                }

                // add to the CSS content
                $style.= $content."\n";
            }
        }

        // extract the style tags des tags style, and remove them in the html code
        preg_match_all('/<style[^>]*>(.*)<\/style[^>]*>/isU', $html, $match);
        $html = preg_replace_callback('/<style[^>]*>(.*)<\/style[^>]*>/isU', [$this, 'removeStyleTag'], $html);

        // analyse each style tags
        foreach ($match[1] as $code) {
            // add to the CSS content
            $code = str_replace('<!--', '', $code);
            $code = str_replace('-->', '', $code);
            $style.= $code."\n";
        }

        //analyse the css content
        $this->analyseStyle($style);

        return $html;
    }

    /**
     * put the same line number for the lexer
     * @param string[] $match
     * @return string
     */
    private function removeStyleTag(array $match)
    {
        $nbLines = count(explode("\n", $match[0]))-1;

        return str_pad('', $nbLines, "\n");
    }
}
