// License: GPL2

//#define _DEBUG
#include "jddebug.h"

#include "dom.h"

#include "jdlib/miscutil.h"
#include "type.h"
#include "tools.h"

#include "skeleton/editcolumns.h"

#include <sstream>

enum
{
    SIZE_OF_RAWDATA = 2 * 1024 * 1024
};

using namespace XML;


// staticなHTMLの要素名リストを再定義
std::set< std::string > Dom::m_static_html_elements;


// コンストラクタ
Dom::Dom( const int type, const std::string& name, const bool html )
    : m_html( html ),
      m_nodeType( type ),
      m_nodeName( name ),
      m_parentNode( nullptr )
{
    // HTMLの場合に空要素として扱う物の内で、使われていそうな物( 小文字で統一 )
    if( html && m_static_html_elements.empty() )
    {
        m_static_html_elements.insert( "base" );
        m_static_html_elements.insert( "br" );
        m_static_html_elements.insert( "hr" );
        m_static_html_elements.insert( "img" );
        m_static_html_elements.insert( "input" );
        m_static_html_elements.insert( "link" );
        m_static_html_elements.insert( "meta" );
    }
}

// デストラクタ
Dom::~Dom() noexcept
{
#ifdef _DEBUG
    std::cout << "~Dom() : " << m_nodeName << ", " << m_childNodes.size() << std::endl;
#endif

    clear();
}

// コピーコンストラクタ
Dom::Dom( const Dom& dom )
    : m_html( dom.m_html ),
      m_nodeType( dom.m_nodeType ),
      m_nodeName( dom.m_nodeName ),
      m_nodeValue( dom.m_nodeValue ),
      m_parentNode( nullptr ),
      m_attributes( dom.m_attributes )
{
    copy_childNodes( dom );
}


//
// 子ノードのクリア
//
void Dom::clear() noexcept
{
    for( Dom* child : m_childNodes )
    {
        if( child ) delete child;
    }
    m_childNodes.clear();
}


//
// XMLの文字列をパースして子ノードを追加する
//
void Dom::parse( const std::string& str )
{
    if( str.empty() || str.size() > SIZE_OF_RAWDATA ) return;

    size_t current_pos = 0;
    const size_t str_length = str.length();

    while( current_pos < str_length )
    {
        //プロパティなどに使う変数
        int type = NODE_TYPE_UNKNOWN;
        std::string name;
        std::string value;
        std::map< std::string, std::string > attributes_pair;
        std::string next_source;

        // "<"を探す
        const std::size_t tag_lt_pos = str.find( '<', current_pos );

        // タグの前のテキストノード
        if( current_pos < tag_lt_pos )
        {
            type = NODE_TYPE_TEXT;
            name = "#text";
            value = str.substr( current_pos, tag_lt_pos - current_pos );
            current_pos = tag_lt_pos;
        }
        // 全てがテキストノード
        else if( tag_lt_pos == std::string::npos )
        {
            type = NODE_TYPE_TEXT;
            name = "#text";
            value = str;
            current_pos = str.length();
        }
        // 要素ノード
        else if( current_pos == tag_lt_pos )
        {
            const std::size_t tag_gt_pos = str.find( '>', tag_lt_pos + 1 );

            current_pos = tag_gt_pos + 1;

            // 開始タグの中身( <element attr="value"> )を取り出す
            std::string open_tag = str.substr( tag_lt_pos + 1, tag_gt_pos - tag_lt_pos - 1 );
            if( open_tag.empty() ) continue;

            // タグの中身がアルファベットで始まっているかチェック
            if( !( open_tag[0] >= 'A' && open_tag[0] <= 'Z' ) && !( open_tag[0] >= 'a' && open_tag[0] <= 'z' ) ) {
                continue;
            }

            // タグ構造が壊れてる場合
            size_t broken_pos = 0;
            if( ( broken_pos = open_tag.find( '<' ) ) != std::string::npos )
            {
                 current_pos += broken_pos;
                 continue;
            }

            // XMLの場合に空要素として扱う要素( <element /> )かどうか
            bool empty_element = false;
            if( ! m_html && open_tag.compare( open_tag.length() - 1, 1, "/" ) == 0 )
            {
                empty_element = true;
                open_tag.erase( open_tag.length() - 1 );
            }

            // 要素名を取り出す
            std::string element_name;
            size_t i = 0;
            const size_t open_tag_length = open_tag.length();
            while( i < open_tag_length
                 && open_tag[i] != '\n'
                 && open_tag[i] != '\t'
                 && open_tag[i] != ' ' )
            {
                element_name += open_tag[i];
                ++i;
            }
            if( element_name.empty() ) continue;

            // 要素ノードのタイプを設定
            type = NODE_TYPE_ELEMENT;

            // 要素名は小文字に統一
            name = MISC::tolower_str( element_name );

            // 属性ペアのリストを作成
            attributes_pair = create_attribute( open_tag.substr( i ) );

            // HTMLの場合に空要素として扱う要素かどうか
            if( m_html && m_static_html_elements.find( name ) != m_static_html_elements.end() ) empty_element = true;

            // 終了タグがある要素からの中身の取り出し( <element>中身</element> )
            if( ! empty_element )
            {
                // count は見つける必要がある終了タグの数
                size_t close_tag_lt_pos = 0, close_tag_gt_pos = 0, count = 1;
                while( ( close_tag_lt_pos = str.find( '<', current_pos ) ) != std::string::npos
                     && ( close_tag_gt_pos = str.find( '>', close_tag_lt_pos + 1 ) ) != std::string::npos )
                {
                    current_pos = close_tag_gt_pos + 1;

                    // タグの中身を取り出す
                    const std::string close_tag = MISC::tolower_str( str.substr( close_tag_lt_pos + 1, close_tag_gt_pos - close_tag_lt_pos - 1 ) );

                    // タグ構造が壊れてる場合
                    if( close_tag.empty() ) continue;
                    else if( ( broken_pos = close_tag.find( '<' ) ) != std::string::npos )
                    {
                         current_pos += broken_pos;
                         continue;
                    }

                    // 空要素でない同名の開始タグを見つけたらカウントを増やす
                    if( close_tag.compare( 0, name.length(), name ) == 0
                     && close_tag.compare( close_tag.length() - 1, 1, "/" ) != 0 ) ++count;
                    // 終了タグを見つけたらカウントを減らす
                    else if( close_tag.compare( 0, name.length() + 1, "/" + name ) == 0 ) --count;

                    // 終了タグを見つける必要数が 0 になったらループを抜ける
                    if( count == 0 ) break;
                }

                // 必要な終了タグが見付からないまま上記のループを抜けた場合は、全体のループを抜ける
                if( count > 0 ) break;

                // 次の再帰で使う"子ノード"の素材になる文字列
                next_source = str.substr( tag_gt_pos + 1, close_tag_lt_pos - tag_gt_pos - 1 );
             }
        }

#ifdef _DEBUG
        std::cout << "Dom:parse():---------------------------------------\n";
        std::cout << "nodeName : " << name << "\n";
        std::cout << "nodeType : " << type << "\n";
        for( const auto& attr : attributes_pair )
        {
            std::cout << "Attribute: " << "name=" << attr.first << ", value=" << attr.second << std::endl;
        }
        std::cout << "nodeValue: " << value << std::endl;
#endif

        // ノードを追加
        Dom* node = appendChild( type, name );
        node->m_attributes = std::move( attributes_pair );
        node->m_nodeValue = std::move( value );

        // 再帰
        if( ! next_source.empty() ) node->parse( next_source );
    }
}


//
// 属性ペアのリストを作成
//
std::map< std::string, std::string > Dom::create_attribute( const std::string& str ) const
{
    std::map< std::string, std::string > attributes_pair;

    if( str.empty() ) return attributes_pair;

    size_t i = 0, str_length = str.length();
    while( i < str_length )
    {
        std::string attr_key;
        std::string attr_value;

        // 余分なスペース等を詰める
        while( i < str_length
             && ( str[i] == '\n'
               || str[i] == '\t'
               || str[i] == ' ' ) ) ++i;

        // "="かスペース等でなければ属性名とする
        while( i < str_length
             && str[i] != '\n'
             && str[i] != '\t'
             && str[i] != ' '
             && str[i] != '=' )
        {
            attr_key += str[i];
            ++i;
        }

        // 余分なスペース等を詰める
        while( i < str_length
             && ( str[i] == '\n'
               || str[i] == '\t'
               || str[i] == ' ' ) ) ++i;

        // "="の区切りがある場合のみ属性の値をセットする
        if( str[i] == '=' )
        {
            // "="の分進める
            ++i;

            // 余分なスペース等を詰める
            while( i < str_length
                 && ( str[i] == '\n'
                   || str[i] == '\t'
                   || str[i] == ' ' ) ) ++i;

            // 属性の値 "〜"
            if( str[i] == '"' )
            {
                ++i;
                while( i < str_length && str[i] != '"' )
                {
                    attr_value += str[i];
                    ++i;
                }
                if( str[i] == '"' ) ++i;
            }
            // 属性の値 '〜'
            else if( str[i] == '\'' )
            {
                ++i;
                while( i < str_length && str[i] != '\'' )
                {
                    attr_value += str[i];
                    ++i;
                }
                if( str[i] == '\'' ) ++i;
            }
            // 属性の値 その他( 本来ならXMLとしてエラー )
            else
            {
                while( i < str_length
                     && str[i] != '\n'
                     && str[i] != '\t'
                     && str[i] != ' ' )
                {
                    attr_value += str[i];
                    ++i;
                }
            }
        }

        // 属性を追加
        if( ! attr_key.empty() && ! attr_value.empty() )
        {
            // 属性名は小文字に統一する
            attributes_pair.insert( make_pair( MISC::tolower_str( attr_key ), attr_value ) );
        }
    }

    return attributes_pair;
}


//
// XMLタグ構造の文字列を生成
//
std::string Dom::get_xml( const int n ) const
{
    std::stringstream xml;

    // インデント
    std::string indent;

    // テキストノードの文字列
    std::string text;

    // ノードの種類別に処理
    switch( m_nodeType )
    {
        // 要素
        case NODE_TYPE_ELEMENT:

            // インデントを追加
            for( int i = 0; i < n; ++i ) indent += " ";

            // タグの始まり
            xml << indent << "<" << m_nodeName;

            // 属性を追加
            for( const auto& attr : m_attributes )
            {
                xml << " " << attr.first << "=\"" << attr.second << "\"";
            }

            // 子要素がある場合
            if( ! m_childNodes.empty() )
            {
                xml << ">\n";

                for( const Dom* child : m_childNodes )
                {
                    // 子要素をたどる
                    xml << child->get_xml( n + 2 );
                }

                xml << indent << "</" << m_nodeName << ">\n";
            }
            // 空要素の場合
            else xml << " />\n";

            break;

        // テキストノード
        case NODE_TYPE_TEXT:

            text = MISC::remove_spaces( m_nodeValue );
            if( ! text.empty() )
            {
                // インデントを追加
                for( int i = 0; i < n; ++i ) indent += " ";
                xml << indent << text << "\n";
            }

            break;

        // ドキュメントノード
        case NODE_TYPE_DOCUMENT:

            if( ! m_childNodes.empty() ) xml << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";

            for( const Dom* child : m_childNodes )
            {
                // 子要素をたどる
                xml << child->get_xml();
            }

            break;
    }

    return xml.str();
}


//
// Gtk::TreeModel::Children からノードツリーを生成
//
// ただし列は SKELETON::EditColumns を継承したものであること
//
void Dom::parse( const Gtk::TreeModel::Children& children, SKELETON::EditColumns& columns )
{
    if( children.empty() ) return;

    // Gtk::TreeModel::Children を走査
    for( const Gtk::TreeModel::Row& row : children )
    {
        // 各値を取得( skeleton/editcolumns.h を参照 )
        const int type = row[ columns.m_type ];
        const Glib::ustring& url = row[ columns.m_url ];
        const Glib::ustring& data = row[ columns.m_data ];
        const Glib::ustring& name = row[ columns.m_name ];
        const size_t dirid = row[ columns.m_dirid ];
        const bool expand = row[ columns.m_expand ];

        if( type != TYPE_UNKNOWN )
        {
            // タイプにより要素名を決定( board や link など)
            const std::string node_name = XML::get_name( type );

            if( ! node_name.empty() )
            {
                Dom* node = appendChild( NODE_TYPE_ELEMENT, node_name );
                if( type == TYPE_DIR && expand ) node->setAttribute( "open", "y" );
                if( ! name.empty() ) node->setAttribute( "name", name );
                if( ! url.empty() ) node->setAttribute( "url", url );
                if( ! data.empty() ) node->setAttribute( "data", data );
                if( dirid ) node->setAttribute( "dirid", dirid );

                // 再帰
                const Gtk::TreeModel::Children sub_children = row.children();
                if( ! sub_children.empty() ) node->parse( sub_children, columns );
            }
        }
    }
}


//
// ノードを分解して Gtk::TreeStore へ Gtk::TreeModel::Row を追加
//
// ただし列は SKELETON::EditColumns を継承したものであること
//
// list_path_expand は開いてるツリーを格納するための参照渡し
//
void Dom::append_treestore( Glib::RefPtr< Gtk::TreeStore >& treestore,
                            SKELETON::EditColumns& columns,
                            std::list< Gtk::TreePath >& list_path_expand,
                            const Gtk::TreeModel::Row& parent ) const
{
    // ノードの子要素を走査
    for( const Dom* child : m_childNodes )
    {
        const int node_type = child->nodeType();

        if( node_type == NODE_TYPE_ELEMENT )
        {
            const int type = XML::get_type( child->nodeName() );

            if( type != TYPE_UNKNOWN )
            {
                Gtk::TreeModel::Row row;

                // Gtk::TreeStore::append() の追加ポイント
                if( parent ) row = *( treestore->append( parent.children() ) );
                else row = *( treestore->append() );

                // 各値をセット
                columns.setup_row( row, child->getAttribute( "url" ), child->getAttribute( "name" ),
                                   child->getAttribute( "data" ), type, 0 );

                if( type == TYPE_DIR ){

                    row[ columns.m_dirid ] = atoi( child->getAttribute( "dirid" ).c_str() );

                    // 開いているツリーを追加
                    if( child->getAttribute( "open" ) == "y" ) list_path_expand.push_back( treestore->get_path( row ) );
                }

                // 再帰
                if( child->hasChildNodes() ) child->append_treestore( treestore, columns, list_path_expand, row );
            }
        }
    }
}


//
// getElementById()
//
Dom* Dom::getElementById( const std::string& id ) const
{
    Dom* node = nullptr;

    for( Dom* child : m_childNodes )
    {
        if( child->nodeType() == NODE_TYPE_ELEMENT )
        {
            if( child->getAttribute( "id" ) == id ) node = child;
            // 再帰
            else if( child->hasChildNodes() ) node = child->getElementById( id );

            if( node ) break;
        }
    }

    return node;
}


//
// getElementsByTagName()
//
std::list<Dom*> Dom::getElementsByTagName( const std::string& name ) const
{
    std::list<Dom*> domlist;

    for( Dom* child : m_childNodes )
    {
        if( child->nodeType() == NODE_TYPE_ELEMENT )
        {
            if( child->nodeName() == name ) domlist.push_back( child );

            // 再帰
            std::list<Dom*> sub_nodes = child->getElementsByTagName( name );
            domlist.splice( domlist.end(), std::move( sub_nodes ) );
        }
    }

    return domlist;
}


//
// ノード：hasChildNodes
//
bool Dom::hasChildNodes() const noexcept
{
    return ! m_childNodes.empty();
}


//
// dom の子ノードをコピーする
//
void Dom::copy_childNodes( const Dom& dom )
{
    clear();

    for( const Dom* child : dom.m_childNodes )
    {
        Dom* node = new Dom( child->m_nodeType, child->m_nodeName );
        node->m_nodeValue = child->m_nodeValue;
        node->m_parentNode = this;
        node->m_attributes = child->m_attributes;
        node->copy_childNodes( *child );

        m_childNodes.push_back( node );
    }
}


//
// ノード：firstChild
//
Dom* Dom::firstChild() const
{
    if( m_childNodes.empty() ) return nullptr;

    return m_childNodes.front();
}


//
// ノード：appendChild()
//
Dom* Dom::appendChild( const int node_type, const std::string& node_name )
{
    Dom* node = new Dom( node_type, node_name, m_html );
    node->m_parentNode = this;
    m_childNodes.push_back( node );

    return node;
}


//
// ノード：removeChild()
//
bool Dom::removeChild( Dom* node )
{
    if( ! node ) return false;

    m_childNodes.remove( node );
    delete node;

    return true;
}


//
// ノード：insertBefore()
//
Dom* Dom::insertBefore( const int node_type, const std::string& node_name, Dom* insNode )
{
    if( ! insNode ) return appendChild( node_type, node_name );

    Dom* newNode = nullptr;

    const auto it = std::find( m_childNodes.begin(), m_childNodes.end(), insNode );
    if( it != m_childNodes.end() )
    {
        newNode = new Dom( node_type, node_name );
        newNode->m_parentNode = insNode->m_parentNode;
        m_childNodes.insert( it, newNode );
    }
    return newNode;
}


//
// 属性：getAttribute()
//
std::string Dom::getAttribute( const std::string& name ) const
{
    std::string value;

    if( name.empty() ) return value;

    std::map< std::string, std::string >::const_iterator it = m_attributes.find( name );

    if( it != m_attributes.cend() ) value = MISC::html_unescape( (*it).second );

    return value;
}


//
// 属性：setAttribute( std::string )
//
bool Dom::setAttribute( const std::string& name, const std::string& value )
{
    if( name.empty()
     || value.empty()
     || m_nodeType != NODE_TYPE_ELEMENT ) return false;

    // 属性名は小文字に統一
    const std::string attr_name = MISC::tolower_str( name );

    m_attributes.insert( make_pair( attr_name, MISC::html_escape( value ) ) );

    return true;
}


//
// 属性：setAttribute( int )
//
bool Dom::setAttribute( const std::string& name, const int value )
{
    if( name.empty()
     || m_nodeType != NODE_TYPE_ELEMENT ) return false;

    // 属性名は小文字に統一
    const std::string attr_name = MISC::tolower_str( name );

    m_attributes.insert( make_pair( attr_name, std::to_string( value ) ) );

    return true;
}
