/*
 * Copyright (c) 2010-2013 Stefan Bolte <portix@gmx.net>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include <string.h>
#include <JavaScriptCore/JavaScript.h>
#include "dwb.h"
#include "util.h"
#include "domain.h"
#include "adblock.h"
#include "js.h"
#include "dom.h"



/* DECLARATIONS {{{*/
/* Type definitions {{{*/
typedef enum _AdblockOption {
    AO_BEGIN              = 1<<2,
    AO_BEGIN_DOMAIN       = 1<<3,
    AO_END                = 1<<4,
    AO_MATCH_CASE         = 1<<7,
    AO_THIRDPARTY         = 1<<8,
    AO_NOTHIRDPARTY       = 1<<9,
} AdblockOption;
/*  Attributes */
typedef enum _AdblockAttribute {
    AA_SCRIPT             = 1<<0,
    AA_IMAGE              = 1<<1,
    AA_STYLESHEET         = 1<<2,
    AA_OBJECT             = 1<<3,
    AA_XBL                = 1<<4,
    AA_PING               = 1<<5,
    AA_XMLHTTPREQUEST     = 1<<6,
    AA_OBJECT_SUBREQUEST  = 1<<7,
    AA_DTD                = 1<<8,
    AA_SUBDOCUMENT        = 1<<9,
    AA_DOCUMENT           = 1<<10,
    AA_ELEMHIDE           = 1<<11,
    AA_OTHER              = 1<<12,
    /* inverse */
} AdblockAttribute;
#define AA_CLEAR_FRAME(X) (X & ~(AA_SUBDOCUMENT|AA_DOCUMENT))

#define AB_INVERSE 15
/* clear upper bits, last attribute may be 1<<14 */
#define AB_CLEAR_UPPER 0x7fff
#define AB_CLEAR_LOWER 0x3fff8000

typedef struct _AdblockRule {
    GRegex *pattern;
    AdblockOption options;
    AdblockAttribute attributes;
    char **domains;
} AdblockRule;

typedef struct _AdblockElementHider {
    char *selector;
    char **domains;
    gboolean exception;
} AdblockElementHider;
/*}}}*/

/* Static variables {{{*/
static GPtrArray *s_simple_rules;
static GPtrArray *s_simple_exceptions;
static GPtrArray *s_rules;
static GPtrArray *s_exceptions;
static GHashTable *s_hider_rules;
gboolean s_has_hider_rules;
/*  only used to freeing elementhider */
static GSList *s_hider_list;
static GString *s_css_exceptions;
static gboolean s_init = false;
static GSList *s_css_hider_list;
#define HIDER_LIST_MAX 3000
/*}}}*//*}}}*/

/* NEW AND FREE {{{*/
/* adblock_rule_new {{{*/
static AdblockRule *
adblock_rule_new() 
{
    AdblockRule *rule = dwb_malloc(sizeof(AdblockRule));
    rule->pattern = NULL;
    rule->options = 0;
    rule->attributes = 0;
    rule->domains = NULL;
    return rule;
}/*}}}*/

/* adblock_rule_free {{{*/
static void 
adblock_rule_free(AdblockRule *rule) 
{
    if (rule->pattern != NULL) 
        g_regex_unref(rule->pattern);

    if (rule->domains != NULL) 
        g_strfreev(rule->domains);
    
    g_free(rule);
}/*}}}*/

/* adblock_element_hider_new {{{*/
static AdblockElementHider *
adblock_element_hider_new(const char *selector, char **domains) 
{
    AdblockElementHider *hider = dwb_malloc(sizeof(AdblockElementHider));
    hider->selector = g_strdup(selector);
    hider->domains = domains;
    return hider;
}/*}}}*/

/* adblock_element_hider_free {{{*/
static void
adblock_element_hider_free(AdblockElementHider *hider) 
{
    if (hider) 
    {
        if (hider->selector) 
            g_free(hider->selector);
        
        if (hider->domains) 
            g_strfreev(hider->domains);
        
        g_free(hider);
    }
}/*}}}*//*}}}*/


/* MATCH {{{*/
/* inline adblock_do_match(AdblockRule *, const char *) {{{*/
static inline gboolean
adblock_do_match(AdblockRule *rule, const char *uri) 
{
    if (g_regex_match(rule->pattern, uri, 0, NULL)) 
    {
        PRINT_DEBUG("blocked %s %s\n", uri, g_regex_get_pattern(rule->pattern));
        return true;
    }
    return false;
}/*}}}*/

/* adblock_match(GPtrArray *, SoupURI *, const char *base_domain, * AdblockAttribute, gboolean thirdparty)  {{{
 * Params: 
 * array      - the filter array
 * uri        - the uri to check
 * uri_host   - the hostname of the request
 * uri_base   - the domainname of the request
 * host       - the hostname of the page
 * domain     - the domainname of the page
 * thirdparty - thirdparty request ? 
 * */
gboolean                
adblock_match(GPtrArray *array, const char *uri, const char *uri_host, const char *uri_base, const char *host, const char *domain, AdblockAttribute attributes, gboolean thirdparty) 
{
    if (array->len == 0)
        return false;
    const char *base_start = strstr(uri, uri_base);
    const char *uri_start = strstr(uri, uri_host);
    const char *suburis[SUBDOMAIN_MAX];
    int uc = 0;
    const char *cur = uri_start;
    const char *nextdot;
    AdblockRule *rule;
    /* Get all suburis */
    suburis[uc++] = cur;
    while (cur != base_start) 
    {
        nextdot = strchr(cur, '.');
        cur = nextdot + 1;
        suburis[uc++] = cur;
        if (uc == SUBDOMAIN_MAX-1)
            break;
    }
    suburis[uc++] = NULL;

    for (guint i=0; i<array->len; i++) 
    {
        rule = g_ptr_array_index(array, i);
        if ( (attributes & AA_DOCUMENT && !(rule->attributes & AA_DOCUMENT)) || (attributes & AA_SUBDOCUMENT && !(rule->attributes & AA_SUBDOCUMENT)) )
            continue;
        /* If exception attributes exists, check if exception is matched */
        if (AA_CLEAR_FRAME(rule->attributes) & AB_CLEAR_LOWER && (AA_CLEAR_FRAME(rule->attributes) == (AA_CLEAR_FRAME(attributes)<<AB_INVERSE))) 
            continue;
        /* If attribute restriction exists, check if attribute is matched */
        if (AA_CLEAR_FRAME(rule->attributes) & AB_CLEAR_UPPER && (AA_CLEAR_FRAME(rule->attributes) != AA_CLEAR_FRAME(attributes))) 
            continue;
        if (rule->domains && !domain_match(rule->domains, host, domain)) 
            continue;
        if    ( (rule->options & AO_THIRDPARTY && !thirdparty) 
                ||  (rule->options & AO_NOTHIRDPARTY && thirdparty) )
            continue;
        if (rule->options & AO_BEGIN_DOMAIN)  
        {
            for (int i=0; suburis[i]; i++) 
            {
                if ( adblock_do_match(rule, suburis[i]) ) 
                    return true;
            }
        }
        else if (adblock_do_match(rule, uri)) 
            return true;
    }
    return false;
}/*}}}*/

/* adblock_prepare_match (const char *uri, const char *baseURI, AdblockAttribute attributes {{{ */
static gboolean
adblock_prepare_match(const char *uri, const char *baseURI, AdblockAttribute attributes) 
{
    char *realuri = NULL;
    SoupURI *suri = NULL, *sbaseuri = NULL;
    gboolean ret = false;

    if (! g_regex_match_simple("^https?://", uri, 0, 0)) 
    {
        gboolean last_slash = g_str_has_suffix(baseURI, "/");
        if (*uri == '/' && last_slash) 
            realuri = g_strconcat(baseURI, uri+1, NULL);
        else if (*uri != '/' && !last_slash)
            realuri = g_strconcat(baseURI, "/", uri, NULL);
        else 
            realuri = g_strconcat(baseURI, uri, NULL);
    }
    else 
        realuri = g_strdup(uri);

    /* FIXME: soup_uri_get_host is just used to get parse the uri */
    suri = soup_uri_new(realuri);
    if (suri == NULL) 
        goto error_out;
    const char *host = soup_uri_get_host(suri);
    if (host == NULL)
        goto error_out;

    sbaseuri = soup_uri_new(baseURI);
    if (sbaseuri == NULL) 
        goto error_out;

    const char *basehost = soup_uri_get_host(sbaseuri);
    if (basehost == NULL)
        goto error_out;

    const char *domain = domain_get_base_for_host(host);
    const char *basedomain = domain_get_base_for_host(basehost);
    gboolean thirdparty = g_strcmp0(domain, basedomain);

    if (!adblock_match(s_exceptions, realuri, host, domain, basehost, basedomain, attributes, thirdparty)) 
    {
        if (adblock_match(s_rules, realuri, host, domain, basehost, basedomain, attributes, thirdparty)) 
            ret = true;
    }
error_out:
    if (realuri != NULL) g_free(realuri);
    if (sbaseuri != NULL) soup_uri_free(sbaseuri);
    if (suri != NULL) soup_uri_free(suri);
    return ret;
}/*}}}*/

/* adblock_apply_element_hider(WebKitWebFrame *frame, GList *gl) {{{*/
void 
adblock_apply_element_hider(WebKitWebFrame *frame, GList *gl) 
{
    GSList *list;

    WebKitWebDataSource *datasource = webkit_web_frame_get_data_source(frame);
    WebKitNetworkRequest *request = webkit_web_data_source_get_request(datasource);

    SoupMessage *msg = webkit_network_request_get_message(request);
    if (msg == NULL)
        return;

    SoupURI *suri = soup_message_get_first_party(msg);
    g_return_if_fail(suri != NULL);

    const char *host = soup_uri_get_host(suri);
    const char *base_domain = domain_get_base_for_host(host);
    g_return_if_fail(host != NULL);
    g_return_if_fail(base_domain != NULL);

    AdblockElementHider *hider;
    GString *css_rule = g_string_new(NULL);

    /* get all subdomains */
    const char *subdomains[SUBDOMAIN_MAX];
    char *nextdot = NULL;
    int uc = 0;
    const char *tmphost = host;
    subdomains[uc++] = tmphost;
    while (tmphost != base_domain) 
    {
        nextdot = strchr(tmphost, '.');
        tmphost = nextdot + 1;
        subdomains[uc++] = tmphost;
        if (uc == SUBDOMAIN_MAX-1)
            break;
    }
    subdomains[uc++] = NULL;

    gboolean has_exception = false;
    for (int i=0; subdomains[i]; i++) 
    {
        list = g_hash_table_lookup(s_hider_rules, subdomains[i]);
        if (list) 
        {
            for (GSList *l = list; l; l=l->next) 
            {
                hider = l->data;
                if (hider->exception) 
                    has_exception = true;
                else if  (domain_match(hider->domains, host, base_domain)) 
                {
                    g_string_append(css_rule, hider->selector);
                    g_string_append_c(css_rule, ',');
                }
            }
            break;
        }
    }
    /* Adding replaced exceptions */
    if (! has_exception) 
        g_string_append(css_rule, s_css_exceptions->str);
    
    if (css_rule->str[css_rule->len-1] == ',') 
        g_string_erase(css_rule, css_rule->len-1, 1);

    g_string_append(css_rule, "{display:none!important;}");

    if (frame == webkit_web_view_get_main_frame(WEBVIEW(gl))) 
    {
        WebKitDOMDocument *doc = webkit_web_view_get_dom_document(WEBVIEW(gl));
        WebKitDOMHTMLHeadElement *head = webkit_dom_document_get_head(doc);

        g_return_if_fail(G_IS_OBJECT(head));

        for (GSList *l = VIEW(gl)->status->styles; l; l=l->next) 
            webkit_dom_node_append_child(WEBKIT_DOM_NODE(head), WEBKIT_DOM_NODE(l->data), NULL);
        
        if (css_rule->len > 0) 
        {
            webkit_dom_html_element_set_inner_html(WEBKIT_DOM_HTML_ELEMENT(VIEW(gl)->status->exc_style), 
                    css_rule->str, NULL);
            webkit_dom_node_append_child(WEBKIT_DOM_NODE(head), WEBKIT_DOM_NODE(VIEW(gl)->status->exc_style), NULL);
        }
    }
    else 
    {
        if (css_rule->len > 0) 
        {
            js_call_as_function(frame, VIEW(gl)->js_base, "insertAdblockRule", css_rule->str, kJSTypeString, NULL);
        }
        for (GSList *l = s_css_hider_list; l; l=l->next) 
            js_call_as_function(frame, VIEW(gl)->js_base, "insertAdblockRule", l->data, kJSTypeString, NULL);
    }
    g_string_free(css_rule, true);
}/*}}}*/
/*}}}*/


/* LOAD_CALLBACKS {{{*/

/* adblock_before_load_cb  (domcallback) {{{*/
static gboolean
adblock_before_load_cb(WebKitDOMDOMWindow *win, WebKitDOMEvent *event, GList *gl) 
{
    WebKitDOMElement *src = (void*)webkit_dom_event_get_src_element(event);
    char *tagname = webkit_dom_element_get_tag_name(src);
    const char *url = NULL;

    gboolean ret = false;

    WebKitDOMDocument *doc = webkit_dom_dom_window_get_document(win);
    char *baseURI = webkit_dom_document_get_document_uri(doc);

    WebKitDOMDocument *main_doc = webkit_web_view_get_dom_document(WEBVIEW(gl));
    WebKitDOMDOMWindow *main_win = webkit_dom_document_get_default_view(main_doc);
    AdblockAttribute attributes = win == main_win ? AA_DOCUMENT : AA_SUBDOCUMENT;

    if (webkit_dom_element_has_attribute(src, "src")) 
        url = webkit_dom_element_get_attribute(src, "src");
    else if (webkit_dom_element_has_attribute(src, "href")) 
        url = webkit_dom_element_get_attribute(src, "href");
    else if (webkit_dom_element_has_attribute(src, "data")) 
        url = webkit_dom_element_get_attribute(src, "data");
    if (url == NULL) 
        goto error_out;

    if (!g_strcmp0(tagname, "IMG")) 
        attributes |= AA_IMAGE;
    else if (!g_strcmp0(tagname, "SCRIPT"))
        attributes |= AA_SCRIPT;
    else if (!g_strcmp0(tagname, "LINK")  ) 
    {
        char *rel  = webkit_dom_html_link_element_get_rel((void*)src);
        char *type  = webkit_dom_element_get_attribute(src, "type");
        if (!g_strcmp0(rel, "stylesheet") || !g_strcmp0(type, "text/css")) 
            attributes |= AA_STYLESHEET;

        g_free(rel);
        g_free(type);
    }
    else if (!g_strcmp0(tagname, "OBJECT") || ! g_strcmp0(tagname, "EMBED")) 
        attributes |= AA_OBJECT;

    if (adblock_prepare_match(url, baseURI, attributes)) 
        webkit_dom_event_prevent_default(event);
    
    ret = true;
error_out:
    g_object_unref(src);
    g_free(tagname);
    g_free(baseURI);
    return ret;
}/*}}}*/

static void 
adblock_frame_load_status_cb(WebKitWebFrame *frame, GParamSpec *p, GList *gl) 
{
    WebKitLoadStatus status = webkit_web_frame_get_load_status(frame);
    if (status == WEBKIT_LOAD_FIRST_VISUALLY_NON_EMPTY_LAYOUT) 
        adblock_apply_element_hider(frame, gl);
    else if (status == WEBKIT_LOAD_COMMITTED) 
        dom_add_frame_listener(frame, "beforeload", G_CALLBACK(adblock_before_load_cb), true, gl);
}

/* adblock_frame_created_cb {{{*/
void 
adblock_frame_created_cb(WebKitWebView *wv, WebKitWebFrame *frame, GList *gl) 
{
    g_signal_connect(frame, "notify::load-status", G_CALLBACK(adblock_frame_load_status_cb), gl);
}/*}}}*/
/* adblock_resource_request_cb {{{*/
static void 
adblock_resource_request_cb(WebKitWebView *wv, WebKitWebFrame *frame,
    WebKitWebResource *resource, WebKitNetworkRequest *request,
    WebKitNetworkResponse *response, GList *gl) 
{
    if (request == NULL) 
        return;

    AdblockAttribute attribute = webkit_web_view_get_main_frame(wv) == frame ? AA_DOCUMENT : AA_SUBDOCUMENT;

    const char *uri = webkit_network_request_get_uri(request);
    if (uri == NULL)
        return;

    SoupMessage *msg = webkit_network_request_get_message(request);
    if (msg == NULL)
        return;

    SoupURI *suri = soup_message_get_uri(msg);
    const char *host = soup_uri_get_host(suri);
    if (host == NULL)
        return;

    const char *domain = domain_get_base_for_host(host);
    if (domain == NULL)
        return;

    SoupURI *sfirst_party = soup_message_get_first_party(msg);
    if (sfirst_party == NULL)
        return;

    const char *firsthost = soup_uri_get_host(sfirst_party);
    if (firsthost == NULL)
        return;

    const char *firstdomain = domain_get_base_for_host(firsthost);
    if (firstdomain == NULL)
        return;

    gboolean thirdparty = g_strcmp0(domain, firstdomain);

    if (!adblock_match(s_simple_exceptions, uri, host, domain, firsthost, firstdomain, attribute, thirdparty)) 
    {
        if (adblock_match(s_simple_rules, uri, host, domain, firsthost, firstdomain, attribute, thirdparty)) 
            webkit_network_request_set_uri(request, "about:blank");
    }
}/*}}}*/
 
/* adblock_load_status_cb(WebKitWebView *, GParamSpec *, GList *) {{{*/
static void
adblock_load_status_cb(WebKitWebView *wv, GParamSpec *p, GList *gl) 
{
    WebKitLoadStatus status = webkit_web_view_get_load_status(wv);
    if (status == WEBKIT_LOAD_COMMITTED) 
    {
        WebKitDOMDocument *doc = webkit_web_view_get_dom_document(wv);
        WebKitDOMDOMWindow *win = webkit_dom_document_get_default_view(doc);
        webkit_dom_event_target_add_event_listener(WEBKIT_DOM_EVENT_TARGET(win), "beforeload", G_CALLBACK(adblock_before_load_cb), true, gl);
    }
    else if (status == WEBKIT_LOAD_FIRST_VISUALLY_NON_EMPTY_LAYOUT) 
    {
        WebKitWebFrame *frame = webkit_web_view_get_main_frame(wv);
        adblock_apply_element_hider(frame, gl);
    }
}/*}}}*//*}}}*/


/* START AND END {{{*/

gboolean
adblock_running() {
  return s_init && GET_BOOL("adblocker");
}

/* adblock_disconnect(GList *) {{{*/
void 
adblock_disconnect(GList *gl) 
{
    View *v = VIEW(gl);
    if (v->status->signals[SIG_AD_LOAD_STATUS] > 0)  
    {
        g_signal_handler_disconnect(WEBVIEW(gl), VIEW(gl)->status->signals[SIG_AD_LOAD_STATUS]);
        v->status->signals[SIG_AD_LOAD_STATUS] = 0;
    }
    if (v->status->signals[SIG_AD_FRAME_CREATED] > 0) 
    {
        g_signal_handler_disconnect(WEBVIEW(gl), (VIEW(gl)->status->signals[SIG_AD_FRAME_CREATED]));
        v->status->signals[SIG_AD_FRAME_CREATED] = 0;
    }
    if (v->status->signals[SIG_AD_RESOURCE_REQUEST] > 0) 
    {
        g_signal_handler_disconnect(WEBVIEW(gl), (VIEW(gl)->status->signals[SIG_AD_RESOURCE_REQUEST]));
        v->status->signals[SIG_AD_RESOURCE_REQUEST] = 0;
    }
}/*}}}*/

/* adblock_connect() {{{*/
void 
adblock_connect(GList *gl) 
{
    if (!s_init && !adblock_init()) 
        return;
    if (s_rules->len > 0 || s_css_hider_list != NULL || s_has_hider_rules) 
    {
        VIEW(gl)->status->signals[SIG_AD_LOAD_STATUS] = g_signal_connect(WEBVIEW(gl), "notify::load-status", G_CALLBACK(adblock_load_status_cb), gl);
        VIEW(gl)->status->signals[SIG_AD_FRAME_CREATED] = g_signal_connect(WEBVIEW(gl), "frame-created", G_CALLBACK(adblock_frame_created_cb), gl);
    }
    if (s_simple_rules->len > 0) 
        VIEW(gl)->status->signals[SIG_AD_RESOURCE_REQUEST] = g_signal_connect(WEBVIEW(gl), "resource-request-starting", G_CALLBACK(adblock_resource_request_cb), gl);
    
    WebKitDOMDocument *doc = webkit_web_view_get_dom_document(WEBVIEW(gl));
    for (GSList *l = s_css_hider_list; l; l=l->next) 
    {
        WebKitDOMElement *style = webkit_dom_document_create_element(doc, "style", NULL);
        webkit_dom_html_element_set_inner_html(WEBKIT_DOM_HTML_ELEMENT(style), l->data, NULL);
        VIEW(gl)->status->styles = g_slist_prepend(VIEW(gl)->status->styles, style);
    }
    VIEW(gl)->status->exc_style = webkit_dom_document_create_element(doc, "style", NULL);

}/*}}}*/

/* adblock_warn_ignored(const char *message, const char *rule){{{*/
static void 
adblock_warn_ignored(const char *message, const char *rule) 
{
    fprintf(stderr, "Adblock warning: %s\n", message);
    fprintf(stderr, "Adblock warning: Rule %s will be ignored\n", rule);
}/*}}}*/

/* adblock_rule_parse(char *filterlist)  {{{*/
static void
adblock_rule_parse(char *filterlist) 
{
    char **lines = NULL;
    if  (g_file_test(filterlist, G_FILE_TEST_IS_DIR)) 
    {
        GString *string = g_string_new(NULL);
        util_get_directory_content(string, filterlist, NULL);
        if (string->str) 
            lines = g_strsplit(string->str, "\n", -1);

        g_string_free(string, true);
    }
    else 
        lines = util_get_lines(filterlist);

    if (lines == NULL)
        return;

    char *pattern;
    GError *error = NULL;
    char **domain_arr = NULL;
    char *domains;
    const char *domain;
    const char *tmp;
    const char *option_string;
    const char *o;
    char *tmp_a, *tmp_b, *tmp_c;
    int length = 0;
    int option, attributes, inverse;
    gboolean exception;
    GRegex *rule;
    char **options_arr;
    char warning[256];
    int n_css_rules = 0;
    GString *css_rule = g_string_new(NULL);

    for (int i=0; lines[i] != NULL; i++) 
    {
        pattern = lines[i];

        //DwbStatus ret = STATUS_OK;
        GRegexCompileFlags regex_flags = G_REGEX_OPTIMIZE | G_REGEX_CASELESS;
        g_strchomp(pattern);
        util_str_chug(pattern);
        if (*pattern == '\0' || *pattern == '!' || *pattern == '[') 
            continue;

        tmp_a = tmp_b = tmp_c = NULL;
        /* Element hiding rules */
        if ( (tmp = strstr(pattern, "##")) != NULL) 
        {
            /* Match domains */
            if (*pattern != '#') 
            {
                domains = g_strndup(pattern, tmp-pattern);
                domain_arr = g_strsplit(domains, ",", -1);

                AdblockElementHider *hider = adblock_element_hider_new(tmp+2, domain_arr);
                GSList *list;
                gboolean hider_exc = true;
                for (; *domain_arr; domain_arr++) 
                {
                    domain = *domain_arr;
                    if (*domain == '~')
                        domain++;
                    else 
                        hider_exc = false;
                    list = g_hash_table_lookup(s_hider_rules, domain);
                    if (list == NULL) 
                    {
                        list = g_slist_append(list, hider);
                        g_hash_table_insert(s_hider_rules, g_strdup(domain), list);
                    }
                    else 
                    {
                        list = g_slist_append(list, hider);
                        (void) list;
                    }
                    s_has_hider_rules = true;
                }
                hider->exception = hider_exc;
                if (hider_exc) 
                {
                    g_string_append(s_css_exceptions, tmp + 2);
                    g_string_append_c(s_css_exceptions, ',');
                }
                s_hider_list = g_slist_append(s_hider_list, hider);
                g_free(domains);
            }
            /* general rules */
            else 
            {
                g_string_append(css_rule, tmp+2);
                n_css_rules++;
                if (n_css_rules == HIDER_LIST_MAX) 
                {
                    g_string_append(css_rule, "{display:none!important;}");
                    s_css_hider_list = g_slist_prepend(s_css_hider_list, css_rule->str);
                    n_css_rules = 0;
                    g_string_free(css_rule, false);
                    css_rule = g_string_new(NULL);
                }
                else 
                    g_string_append_c(css_rule, ',');
            }
        }
        /*  Request patterns */
        else 
        { 
            exception = false;
            option = 0;
            attributes = 0;
            rule = NULL;
            domain_arr = NULL;
            /* Exception */
            tmp = pattern;
            if (tmp[0] == '@' && tmp[1] == '@') 
            {
                exception = true;
                tmp +=2;
            }
            option_string = strstr(tmp, "$");
            if (option_string != NULL) 
            {
                tmp_a = g_strndup(tmp, option_string - tmp);
                options_arr = g_strsplit(option_string+1, ",", -1);
                for (int i=0; options_arr[i] != NULL; i++) 
                {
                    inverse = 0;
                    o = options_arr[i];
                    /*  attributes */
                    if (*o == '~') 
                    {
                        inverse = AB_INVERSE;
                        o++;
                    }
                    if (!g_strcmp0(o, "script"))
                        attributes |= (AA_SCRIPT << inverse);
                    else if (!g_strcmp0(o, "image"))
                        attributes |= (AA_IMAGE << inverse);
                    else if (!g_strcmp0(o, "stylesheet"))
                        attributes |= (AA_STYLESHEET << inverse);
                    else if (!g_strcmp0(o, "object")) 
                        attributes |= (AA_OBJECT << inverse);
                    else if (!g_strcmp0(o, "subdocument")) 
                        attributes |= inverse ? AA_DOCUMENT : AA_SUBDOCUMENT;
                    else if (!g_strcmp0(o, "document")) 
                    {
                        if (exception) 
                            attributes |= inverse ? AA_DOCUMENT : AA_SUBDOCUMENT;
                        else 
                            adblock_warn_ignored("Adblock option 'document' can only be applied to exception rules", pattern);
                    }
                    else if (!g_strcmp0(o, "match-case"))
                        option |= AO_MATCH_CASE;
                    else if (!g_strcmp0(o, "third-party")) 
                    {
                        if (inverse) 
                            option |= AO_NOTHIRDPARTY;
                        else 
                            option |= AO_THIRDPARTY;
                    }
                    else if (g_str_has_prefix(o, "domain=")) 
                        domain_arr = g_strsplit(options_arr[i] + 7, "|", -1);
                    /* Unsupported should only be ignored if they are actually rules, not
                     * exceptions */
                    else if ((inverse && exception) || (!inverse && !exception)) 
                    {
                        /*  currently unsupported  xbl, ping, xmlhttprequest, dtd, elemhide,
                         *  other, collapse, donottrack, object-subrequest, popup 
                         *  */
                        snprintf(warning, sizeof(warning), "Adblock option '%s' isn't supported", o);
                        adblock_warn_ignored(warning, pattern);
                        goto error_out;
                    }
                }
                tmp = tmp_a;
                g_strfreev(options_arr);
            }
            length = strlen(tmp);
            /* Beginning of pattern / domain */
            if (length > 0 && tmp[0] == '|') 
            {
                if (length > 1 && tmp[1] == '|') 
                {
                    option |= AO_BEGIN_DOMAIN;
                    tmp += 2;
                    length -= 2;
                }
                else 
                {
                    option |= AO_BEGIN;
                    tmp++;
                    length--;
                }
            }
            /* End of pattern */
            if (length > 0 && tmp[length-1] == '|') 
            {
                tmp_b = g_strndup(tmp, length-1);
                tmp = tmp_b;
                option |= AO_END;
                length--;
            }
            /* Regular Expression */
            if (length > 0 && tmp[0] == '/' && tmp[length-1] == '/') 
            {
                tmp_c = g_strndup(tmp+1, length-2);

                if ( (option & AO_MATCH_CASE) != 0) 
                    regex_flags &= ~G_REGEX_CASELESS;
                rule = g_regex_new(tmp_c, regex_flags, 0, &error);

                g_free(tmp_c);
                if (error != NULL) 
                {
                    adblock_warn_ignored("Invalid regular expression", pattern);
                    //ret = STATUS_ERROR;
                    g_clear_error(&error);
                    goto error_out;
                }
            }
            else 
            {
                GString *buffer = g_string_new(NULL);
                if (option & AO_BEGIN || option & AO_BEGIN_DOMAIN) 
                    g_string_append_c(buffer, '^');
                
                /*  FIXME: possibly use g_regex_escape_string */
                for (const char *regexp_tmp = tmp; *regexp_tmp; regexp_tmp++ ) 
                {
                    switch (*regexp_tmp) {
                        case '^' : g_string_append(buffer, "([\\x00-\\x24\\x26-\\x2C\\x2F\\x3A-\\x40\\x5B-\\x5E\\x60\\x7B-\\x80]|$)");
                                   break;
                        case '*' : g_string_append(buffer, ".*");
                                   break;
                        case '?' : 
                        case '{' : 
                        case '}' : 
                        case '(' : 
                        case ')' : 
                        case '[' : 
                        case ']' : 
                        case '+' : 
                        case '.' : 
                        case '\\' : 
                        case '|' : g_string_append_c(buffer, '\\');
                        default  : g_string_append_c(buffer, *regexp_tmp);
                    }
                }
                if (option & AO_END) 
                    g_string_append_c(buffer, '$');
                
                if ( (option & AO_MATCH_CASE) != 0) 
                    regex_flags &= ~G_REGEX_CASELESS;

                rule = g_regex_new(buffer->str, regex_flags, 0, &error);

                g_string_free(buffer, true);

                if (error != NULL) 
                {
                    fprintf(stderr, "dwb warning: ignoring adblock rule %s: %s\n", pattern, error->message);
                    g_clear_error(&error);
                    goto error_out;
                }
            }

            AdblockRule *adrule = adblock_rule_new();
            adrule->attributes = attributes;
            adrule->pattern = rule;
            adrule->options = option;
            adrule->domains = domain_arr;

            if (! (attributes & (AA_DOCUMENT | AA_SUBDOCUMENT)) )
                adrule->attributes |= AA_SUBDOCUMENT | AA_DOCUMENT;

            if (!(attributes & ~(AA_SUBDOCUMENT | AA_DOCUMENT))) 
            {
                if (exception) 
                    g_ptr_array_add(s_simple_exceptions, adrule);
                else 
                    g_ptr_array_add(s_simple_rules, adrule);
            }
            else 
            {
                if (exception) 
                    g_ptr_array_add(s_exceptions, adrule);
                else 
                    g_ptr_array_add(s_rules, adrule);
            }
        }
error_out:
        g_free(tmp_a);
        g_free(tmp_b);
    }
    if (css_rule->len > 0) 
    {
        g_string_erase(css_rule, css_rule->len-1, 1);
        g_string_append(css_rule, "{display:none!important;}");
        s_css_hider_list = g_slist_prepend(s_css_hider_list, css_rule->str);
    }
    g_string_free(css_rule, false);
    g_strfreev(lines);
}/*}}}*/

/* adblock_end() {{{*/
void 
adblock_end() 
{
    for (GSList *l = s_css_hider_list; l; l=l->next) 
        g_free(l->data);
    
    g_slist_free(s_css_hider_list);
    if (s_css_exceptions != NULL) 
        g_string_free(s_css_exceptions, true);
    if (s_rules != NULL) 
        g_ptr_array_free(s_rules, true);
    if (s_simple_rules != NULL) 
        g_ptr_array_free(s_simple_rules, true);
    if (s_simple_exceptions != NULL) 
        g_ptr_array_free(s_simple_exceptions, true);
    if(s_exceptions != NULL) 
        g_ptr_array_free(s_exceptions, true);
    if (s_hider_rules != NULL) 
        g_hash_table_remove_all(s_hider_rules);
    if (s_hider_list != NULL) 
    {
        for (GSList *l = s_hider_list; l; l=l->next) 
            adblock_element_hider_free((AdblockElementHider*)l->data);

        g_slist_free(s_hider_list);
    }
}/*}}}*/

/* adblock_init() {{{*/
gboolean
adblock_init() 
{
    if (s_init)
        return true;
    if (!GET_BOOL("adblocker"))
        return false;

    char *filterlist = GET_CHAR("adblocker-filterlist");
    if (filterlist == NULL)
        return false;

    char buffer[PATH_MAX];

    filterlist = util_expand_home(buffer, filterlist, sizeof(buffer));
    if (!g_file_test(filterlist, G_FILE_TEST_EXISTS)) 
    {
        fprintf(stderr, "Filterlist not found: %s\n", filterlist);
        return false;
    }

    s_rules              = g_ptr_array_new_with_free_func((GDestroyNotify)adblock_rule_free);
    s_exceptions         = g_ptr_array_new_with_free_func((GDestroyNotify)adblock_rule_free);
    s_simple_rules       = g_ptr_array_new_with_free_func((GDestroyNotify)adblock_rule_free);
    s_simple_exceptions  = g_ptr_array_new_with_free_func((GDestroyNotify)adblock_rule_free);
    s_hider_rules        = g_hash_table_new_full((GHashFunc)g_str_hash, (GEqualFunc)g_str_equal, (GDestroyNotify)g_free, NULL);
    s_css_exceptions     = g_string_new(NULL);

    adblock_rule_parse(filterlist);
    s_init = true;

    return true;
}/*}}}*//*}}}*/
