import re
from urllib.request import urlopen

import sqlite3

from tag import Tag


DB_PATH = 'bookmarks.db'

SQL_SELECT_BOOKMARK = "SELECT * FROM bookmarks WHERE id=?"
SQL_INSERT_BOOKMARK = "INSERT INTO bookmarks VALUES (?,?,?,?,?,?,?,?)"
SQL_UPDATE_BOOKMARK = "UPDATE bookmarks SET {:s} WHERE id=?"
SQL_SELECT_CAT_TAGS = "SELECT tag_id FROM category_tags where bookmark_id=?"
SQL_SELECT_DESC_TAGS = "SELECT tag_id FROM description_tags where bookmark_id=?"

HTTP_OKAY = 200

class BookmarkNotFoundError(Exception):
    
    def __init__(self, value):
        self.value = value
        
    def __str__(self, value):
        return repr(self.value)
        

class DBConnectionError(Exception):
    
    def __init__(self, value):
        self.value = value
        
    def __str__(self, value):
        return repr(self.value)
        

class Bookmark(object):
    
    def __init__(self, db_conn, id=None, url=None):
        
        # Make sure the DB connection is valid before doing anything else
        if not db_conn:
            raise DBConnectionError('Invalid database connection object!')
        
        # Setup internal parameters
        self._db_conn = db_conn
        self._content = None
        
        # Setup defaut object data attributes
        self.id = id
        self.url = url
        self.title = ''
        self.description = ''
        self.times_visited = 0
        self.last_visited = 0
        self.last_reachable = 0
        self.deleted = False
        self.category_tags = set()
        self.description_tags = set()
        
        # Retrieve an existing bookmark if an ID is passed in
        if self.id:
            db = self._db_conn.cursor()
            db.execute(SQL_SELECT_BOOKMARK, (self.id,))
            row = db.fetchone()
            
            # Check to see if a record was found
            if not row:
                raise BookmarkNotFoundError(self.id)
            
            # Assign data attributes
            self.url = row[1]
            self.title = row[2]
            self.description = row[3]
            self.times_visited = int(row[4])
            self.last_visited = row[5]
            self.last_reachable = row[6]
            if int(row[7]) == 0:
                self.deleted = False
            else:
                self.deleted = True
                
            # Grab category tags
            for row in db.execute(SQL_SELECT_CAT_TAGS, (self.id,)):
				self.category_tags.add(Tag(int(row[0])))
				
			# Grab description tags
            for row in db.execute(SQL_SELECT_DESC_TAGS, (self.id,)):
				self.description_tags.add(Tag(int(row[0])))
        
        # This dict MUST appear *after* the data attributes, it is used to 
        # record which data attributes are dirty
        self._dirty = {'url': False,
                       'title': False,
                       'description': False,
                       'times_visited': False,
                       'last_visited': False,
                       'last_reachable': False,
                       'deleted': False}
    
    def __del__(self):
        """ Automatically save any changes to a record before the object is 
        destroyed. 
        """
        self.save()
    
    def save(self):
        """ Save any changes to a bookmark record if it is dirty. """
        # Check if the record is dirty
        if self.record_dirty:
            if self.id:
                # Update existing bookmark record
                self._db_update()
            else:
                # Insert new bookmark record
                self._db_insert()
    
    def __setattr__(self, name, value):
        """ Customize setting data attributes so we can tell which ones
        are dirty.
        """
        
        # Check if the attr is a data attribute then check if the value has 
        # changed. If so, mark the attribute as dirty
        if hasattr(self, '_dirty'):
            if name in self._dirty and getattr(self, name) != value:
                self._dirty[name] = True
                
        # Set the attribute value using the super class's __setattr__ to avoid
        # infinite recursion (recursion joke here)
        super(Bookmark, self).__setattr__(name, value)
        
    def _db_insert(self):
        """ Insert a new bookmark record into the database. """
        db = self._db_conn.cursor()
        values = (0, self.url, self.title, self.description, 
                  self.times_visited, self.last_visited, self.last_reachable,
                  self.deleted and '1' or '0')
        db.execute(SQL_INSERT_BOOKMARK, values)
        self._db_conn.commit()
        
        # Reset dirty flags
        for key in self._dirty.keys():
            self._dirty[key] = False
            
    def _db_update(self):
        """ Update an exisitng bookmark record in the database. """
        values = []
        sets = []
        
        # Find dirty data attributes for updating
        for key in self._dirty.keys():
            if self._dirty[key] == True:
                sets.append('{:s}=?'.format(key))
                values.append(getattr(self, key))
        
        if not values or not sets:
            return
        
        # Add the record id as the last value
        values.append(self.id)
        
        # Build full SQL statement then execute it
        sql = SQL_UPDATE_BOOKMARK.format(','.join(sets))
        db = self._db_conn.cursor()
        db.execute(sql, values)
        self._db_conn.commit()
        
        # Reset dirty flags
        for key in self._dirty.keys():
            self._dirty[key] = False
    
    @property
    def record_dirty(self):
        """ Check if any data attributes are dirty, which indicates the record 
        is dirty.
        """
        return any([val for key, val in self._dirty.items()])
        
    def _get_content(self):
        """ Read the page and get its raw page HTML content. """
        if self.url:
            response = urlopen(self.url)
            if response.getcode() == HTTP_OKAY:
                self._content = response.read().decode('utf-8')
        
    def suggest_title(self):
        """ Return the contents of the page's <title> tag as a title suggestion
        """
        if self.url and not self._content:
            self._get_content()
        
        try:
            title = re.search('<title>(.+)</title>', self._content).group(1)
        except IndexError:
            title = None
        return title
        
    def is_reachable(self):
        """ Check if the page is reachable. """
        if self.url:
            response = urlopen(self.url)
            if response.getcode() == HTTP_OKAY:
                return True
        return False
        
            
