"""

Making some sort of interactive Segment Anything Model (SAM) for Whole Slide Image annotation correction

"""

import os
import sys
import numpy as np
import lxml.etree as ET
from geojson import Feature, dump
import json
import geojson

from tqdm import tqdm

import torch
import torchvision

from segment_anything import sam_model_registry, SamPredictor, SamAutomaticMaskGenerator
from segment_anything.utils.transforms import ResizeLongestSide

from PIL import Image

import plotly.express as px
import plotly.graph_objects as go

from dash import dcc, ctx, exceptions
import dash_bootstrap_components as dbc
import dash_leaflet as dl

from dash_extensions.enrich import html, DashProxy
from dash_extensions.javascript import arrow_function

from dataclasses import dataclass, field
from typing import Callable, List, Union
from dash.dependencies import handle_callback_args
from dash.dependencies import Input, Output, State

from wsi_annotations_kit import wsi_annotations_kit as wak

def gen_layout(image):

    main_layout = html.Div([
        html.H1('Segment Anything Model (SAM): for Histology image annotation'),
        html.Hr(),
        html.B(),
        dbc.Row([
            dbc.Col([
                html.H3('Current Image'),
                html.B(),
                dcc.Graph(id='current-image',figure=go.Figure(px.imshow(image))),
                dbc.Row([
                    dbc.Col(html.Div(
                        dbc.Button(
                            'Clear Masks',
                            id = 'clear-butt'
                        )
                    ),md=4),
                    dbc.Col(html.Div(
                        dbc.Button(
                            'Save Mask',
                            id = 'save-butt'
                        )
                    ),md=8)
                ])
            ],md=8),

            dbc.Col([
                html.H3('Annotation Options'),
                html.Hr(),
                html.B(),
                dbc.Card(
                    id = 'ann-options',
                    children = [
                        dbc.CardBody([
                            dbc.Row([
                                dbc.Button(
                                    'Predict All',
                                    id = 'predict-all'
                                )
                            ])
                        ])
                    ]
                )
            ])
        ])
    ])

    return main_layout

class SingleImageTest:
    def __init__(self,
                 app,
                 layout,
                 predictor,
                 mask_generator,
                 current_image):

        self.app = app
        self.app.layout = layout
        self.predictor = predictor
        self.mask_generator = mask_generator
        self.current_image = current_image

        self.app.callback(
            Output('current-image','figure'),
            [Input('current-image','clickData'),
             Input('predict-all','n_clicks'),
             Input('clear-butt','n_clicks')],
             prevent_initial_call=True
        )(self.predict)

        self.app.run_server(host = '0.0.0.0',debug=False,use_reloader=False,port=8050)

    def predict(self,click_coords,butt_click,clear_click):

        if ctx.triggered_id=='current-image':

            new_image = self.predict_from_point(click_coords)

        elif ctx.triggered_id =='predict-all':

            new_image = self.predict_all_regions()

        elif ctx.triggered_id =='clear-butt':
            new_image = self.current_image

        else:
            print(f'Bad triggered_id: {ctx.triggered_id}')
            raise exceptions.PreventUpdate

        return px.imshow(new_image)

    def predict_from_point(self,click_coords):

        if not click_coords is None:
            # Points for input into SAM have to be (x,y)
            point_coords = np.array([[click_coords['points'][0]['x'],click_coords['points'][0]['y']]])

            point_label = np.array([1])

            masks, scores, logits = self.predictor.predict(
                point_coords = point_coords,
                point_labels = point_label,
                multimask_output = True
            )

            highest_score_mask = masks[np.argmax(scores),:,:][:,:,None]

            # Creating mask overlay using PIL
            color_mask_4d = self.generate_mask(highest_score_mask)
            overlaid_mask = Image.fromarray(np.uint8(self.current_image)).convert('RGBA')
            overlaid_mask.paste(color_mask_4d,mask=color_mask_4d)

            return overlaid_mask

        else:
            raise exceptions.PreventUpdate

    def predict_all_regions(self):

        masks = self.mask_generator.generate(self.current_image)

        print(f'shape of masks: {np.shape(masks)}')
        if not len(masks) == 0:

            sorted_anns = sorted(masks,key = (lambda x: x['area']), reverse = True)
            final_mask = np.uint8(np.zeros((sorted_anns[0]['segmentation'].shape[0],sorted_anns[0]['segmentation'].shape[1],4)))

            #print(f'shape of final mask: {np.shape(final_mask)}')
            for ann in tqdm(sorted_anns):

                ann_mask = self.generate_mask(ann['segmentation'][:,:,None])
                #print(f'shape of ann_mask: {np.shape(np.array(ann_mask))}')
                final_mask+=np.array(ann_mask)

            final_mask = Image.fromarray(final_mask)
            overlaid_mask = Image.fromarray(np.uint8(self.current_image)).convert('RGBA')
            overlaid_mask.paste(final_mask,mask=final_mask)

            return overlaid_mask
        else:

            print(f'masks shape is zero')
            return self.current_image

    def generate_mask(self,mask):
        zero_mask = np.where(np.sum(mask,axis=-1)==0,0,128)[:,:,None]

        random_color = [np.random.randint(0,255) for i in range(3)]

        color_mask = np.concatenate((random_color[0]*mask,random_color[1]*mask,random_color[2]*mask),axis=-1)
        color_mask_4d = Image.fromarray(np.uint8(np.concatenate((color_mask,zero_mask),axis=-1)),'RGBA')

        return color_mask_4d

def main():

    sam_checkpoint = './models/sam_vit_h_4b8939.pth'
    model_type = 'vit_h'
    device = 'cpu'

    sam = sam_model_registry[model_type](checkpoint = sam_checkpoint)
    sam.to(device=device)

    predictor = SamPredictor(sam)
    mask_generator = SamAutomaticMaskGenerator(sam)

    ex_image = np.uint8(np.array(Image.open('./examples/ex_img.PNG')))[:,:,0:3]
    print(f'shape of ex_image: {np.shape(ex_image)}')
    predictor.set_image(ex_image)

    main_layout = gen_layout(ex_image)
    main_app = DashProxy(__name__,external_stylesheets = [dbc.themes.LUX])

    sam_app = SingleImageTest(main_app,main_layout,predictor,mask_generator,ex_image)

if __name__=='__main__':
    main()


# Question 1 -->> Describe how you could use  single Python List to implement Three Stack
'''
class MultiStack:
    def __init__(self,stacksize):
        self.numberstacks = 3
        self.custList =  [0] * (stacksize * self.numberstacks)
        self.sizes = [0] * self.numberstacks
        self.stacksize = stacksize

    # to check if the stack is full
    def isFull(self,stacknum):
        if self.sizes[stacknum] == self.stacksize :
            return True
        else:
            return False
    # to check if the stack is empty
    def isEmpty(self,stacknum):
        if self.sizes[stacknum] == 0 :
            return True
        else:
            return False

    # helper method
    def indexOfTop(self,stacknum):
        offset =stacknum * self.stacksize
        return offset + self.sizes[stacknum] - 1

    # enter a elemement in stack
    def push(self,item,stacknum):
        if self.isFull(stacknum):
            return "The stack is full"
        else:
            self.sizes[stacknum] += 1
            self.custList[self.indexOfTop(stacknum)] =item

    # remove the element for the stack
    def pop(self,stacknum):
        if self.isEmpty(stacknum):
            return "The stack is empty"
        else:
            value = self.custList[self.indexOfTop(stacknum)]
            self.custList[self.indexOfTop(stacknum)] = 0
            self.sizes[stacknum] -= 1
            return value

    # to show the first element
    def peek(self,stacknum):
        if self.isEmpty(stacknum):
            return "The stack is empty"
        else:
            value = self.custList[self.indexOfTop(stacknum)]
            return value

# code to execute the above function
customStack = MultiStack(6)
print(customStack.isFull(0))
print(customStack.isEmpty(1))
customStack.push(5,0)
customStack.push(2,0)
customStack.push(4,2)
print(customStack.pop(0))
print(customStack.peek(1))
'''

# Question 2 >> How would you design a stack which, in addition to push and pop, has a function min which
# returns the minimum element? Push, pop and min should all operate in O(1).
'''
class Node():
    def __init__(self, value = None, next = None):
        self.value = value
        self.next = next

    # print method
    def __str__(self):
        string = str(self.value)
        if self.next:
            str += ',' +str(self.next)
        return string

class Stack():
    def __init__(self) -> None:
        self.top = None
        self.minNode = None

    def min(self):
        if not self.minNode:
            return None
        return self.minNode.value

    # push method
    def push(self,item):
        if self.minNode and (self.minNode.value < item):
            self.minNode = Node (value = self.minNode.value, next=self.minNode)
        else:
            self.minNode = Node (value= item, next= self.minNode)
        self.top = Node (value= item,next= self.top)

    # pop method
    def pop(self):
        if not self.top :
            return None
        self.minNode = self.minNode.next
        item = self.top.value
        self.top = self.top.value
        return item

# code to execute the code
customStack = Stack()
customStack.push(5)
# print(customStack.min())
# customStack.push(2)
customStack.push(4)
customStack.push(3)
customStack.pop()
print(customStack.min())
'''

# Question 3 >> Imagine a (literal) stack of plates. If the stack gets too high, it might topple. Therefore, in real
# life, we would likely start a new stack when the previous stack exceeds some threshold.
# Implement a data structure SetOfStacks that mimics this. SetOfStacks should be composed of
# several stacks and should create a new stack once the previous one exceeds capacity,
# SetOfStacks.push() and SetOfStacks.pop() should behave identically to a single stack (that is,
# pop() should return the same values as it would if there were just a single stack).

# Follow Up: Implement a function popAt (int index) which performs a pop operation on a specific sub – stack.
'''
class PlateStack():
    def __init__(self,capacity):
        self.capacity = capacity
        self.stacks = []
    def __str__(self):
        return self.stacks

    # push method
    def push(self,item):
        if len(self.stacks) > 0 and (len(self.stacks[-1])) < self.capacity :
            self.stacks[-1].append(item)
        else:
            self.stacks.append([item])

    # pop method
    def pop(self):
        while len(self.stacks) and len(self.stacks[-1]) == 0 :
            self.stacks.pop()
        if len (self.stacks) == 0 :
            return None
        else:
            return self.stacks[-1].pop()

    # method to pop at a particular stack
    def pop_at(self,stackNumber):
        if len(self.stacks[stackNumber]) > 0:
            return self.stacks[stackNumber].pop()
        else:
            return None
# code to execute the above methods
customStack = PlateStack(2)
customStack.push(2)
customStack.push(1)
customStack.push(3)
customStack.push(4)
print(customStack.pop_at(0))
'''

# Question 4 >> Implement Queue class which implements a queue using two stacks.
'''
class Stack():
    def __init__(self):
        self.list = []

    def __len__(self):
        return len(self.list)
    def push(self,item):
        self.list.append(item)
    def pop(self):
        if len(self.list) == 0:
            return None
        return self.list.pop()

class QueueviaStack():
    def __init__(self):
        self.inStack = Stack()
        self.outStack = Stack()
    def enqueue(self,item):
        self.inStack.push(item)
    def dequeue(self):
        while len(self.inStack):
            self.outStack.push(self.inStack.pop())
        result = self.outStack.pop()
        while len(self.outStack):
            self.inStack.push(self.outStack.pop())
        return result

# code to execute the above method
customQueue = QueueviaStack()
customQueue.enqueue (1)
customQueue.enqueue (2)
customQueue.enqueue (3)
print(customQueue.dequeue())
customQueue.enqueue (4)
print(customQueue.dequeue())
'''

# Question 5 >> An animal shelter, which holds only dogs and cats, operates on a strictly "first in, first out"
# basis. People must adopt either the "oldest" (based on arrival time) of all animals at the
# shelter, or they can select whether they would prefer a dog or a cat (and will receive the
# oldest animal of that type). They cannot select which specific animal they would like. Create
# the data structures to maintain this system and implement operations such as enqueue,
# dequeueAny, dequeueDog, and dequeueCat.

# '''
class AnimalShelter():
    def __init__(self):
        self.cats = []
        self.dogs = []

    # to enter a animal
    def enqueue(self,animal,type):
        if type == 'Cat':
            self.cats.append(animal)
        else:
            self.dogs.append(animal)

    def dequeueCat(self):
        if len(self.cats) == 0:
            return None
        else:
            cat = self.cats.pop(0)
            return cat

    def dequeueDog(self):
        if len(self.dogs) == 0:
            return None
        else:
            dog = self.dogs.pop(0)
            return dog

    def dequeueAny(self):
        if len(self.cats) == 0 :
            result = self.dogs.pop(0)
        else:
            result = self.cats.pop(0)
        return result

# code to execute the above methods
customQueue = AnimalShelter()
customQueue.enqueue('Cat1','Cat')
customQueue.enqueue('Cat2','Cat')
customQueue.enqueue('Dog1','Dog')
customQueue.enqueue('Cat3','Cat')
customQueue.enqueue('Dog2','Dog')
print(customQueue.dequeueAny())

# '''


import datetime
import importlib
import json
import logging
import os
import pytz
import sqlite3
import sys
import time

from pyhindsight import __version__
from pyhindsight.browsers.chrome import Chrome
from pyhindsight.browsers.chrome import CacheEntry
from pyhindsight.browsers.brave import Brave
from pyhindsight.utils import friendly_date, format_plugin_output
import pyhindsight.plugins

log = logging.getLogger(__name__)

class HindsightEncoder(json.JSONEncoder):
    """This JSONEncoder translates several Hindsight HistoryItem classes into
    JSON objects for use in the JSONL output format. It also makes changes
    to field names and values to more closely align with Plaso
    (https://github.com/log2timeline/plaso) output for easier use with
    Timesketch (https://github.com/google/timesketch/).
    """

    @staticmethod
    def base_encoder(history_item):
        item = {'source_short': 'WEBHIST', 'source_long': 'Chrome History',
                'parser': f'hindsight/{__version__}'}
        for key, value in list(history_item.__dict__.items()):
            # Drop any keys that have None as value
            if value is None:
                continue

            if key == 'interpretation' and value == '':
                continue

            if isinstance(value, datetime.datetime):
                value = value.isoformat()

            # JSONL requires utf-8 encoding
            if isinstance(value, bytes) or isinstance(value, bytearray):
                value = value.decode('utf-8', errors='replace')

            if isinstance(key, bytes) or isinstance(key, bytearray):
                key = key.decode('utf-8', errors='replace')

            item[key] = value

        if item.get('timestamp'):
            item['datetime'] = item['timestamp']
            del(item['timestamp'])
        else:
            item['datetime'] = '1970-01-01T00:00:00.000000+00:00'

        return item

    def default(self, obj):
        if isinstance(obj, Chrome.URLItem):
            item = HindsightEncoder.base_encoder(obj)

            item['timestamp_desc'] = 'Last Visited Time'
            item['data_type'] = 'chrome:history:page_visited'
            item['url_hidden'] = 'true' if item['hidden'] else 'false'
            if item['visit_duration'] == 'None':
                del (item['visit_duration'])

            item['message'] = f"{item['url']} ({item['title']}) [count: {item['visit_count']}]"

            del(item['name'], item['row_type'], item['visit_time'],
                item['last_visit_time'], item['hidden'])
            return item

        if isinstance(obj, Chrome.MediaItem):
            item = HindsightEncoder.base_encoder(obj)

            item['timestamp_desc'] = 'Media Playback End'
            item['data_type'] = 'chrome:history:media_playback'

            if item.get('source_title'):
                item['message'] = f"Watched{item['watch_time']} on {item['source_title']} "\
                                  f"(ending at {item['position']}/{item.get('media_duration')}) " \
                                  f"[has_video: {item['has_video']}; has_audio: {item['has_audio']}]"
            else:
                item['message'] = f"Watched{item['watch_time']} on {item['url']} " \
                                  f"[has_video: {item['has_video']}; has_audio: {item['has_audio']}]"

            return item

        if isinstance(obj, Chrome.DownloadItem):
            item = HindsightEncoder.base_encoder(obj)

            item['timestamp_desc'] = 'File Downloaded'
            item['data_type'] = 'chrome:history:file_downloaded'

            item['message'] = f"{item['url']} " \
                              f"({item['full_path'] if item.get('full_path') else item.get('target_path')}). " \
                              f"Received {item['received_bytes']}/{item['total_bytes']} bytes"

            del(item['row_type'], item['start_time'])
            return item

        if isinstance(obj, Chrome.CookieItem):
            item = HindsightEncoder.base_encoder(obj)

            item['data_type'] = 'chrome:cookie:entry'
            item['source_long'] = 'Chrome Cookies'
            if item['row_type'] == 'cookie (accessed)':
                item['timestamp_desc'] = 'Last Access Time'
            elif item['row_type'] == 'cookie (created)':
                item['timestamp_desc'] = 'Creation Time'
            item['host'] = item['host_key']
            item['cookie_name'] = item['name']
            item['data'] = item['value'] if item['value'] != '<encrypted>' else ''
            item['url'] = item['url'].lstrip('.')
            item['url'] = 'https://{}'.format(item['url']) if item['secure'] else 'http://{}'.format(item['url'])
            if item['expires_utc'] == '1970-01-01T00:00:00+00:00':
                del(item['expires_utc'])
            # Convert these from 1/0 to true/false to match Plaso
            item['secure'] = 'true' if item['secure'] else 'false'
            item['httponly'] = 'true' if item['httponly'] else 'false'
            item['persistent'] = 'true' if item['persistent'] else 'false'

            item['message'] = '{} ({}) Flags: [HTTP only] = {} [Persistent] = {}'.format(
                item['url'],
                item['cookie_name'],
                item['httponly'], item['persistent'])

            del(item['creation_utc'], item['last_access_utc'], item['row_type'],
                item['host_key'], item['name'], item['value'])
            return item

        if isinstance(obj, Chrome.AutofillItem):
            item = HindsightEncoder.base_encoder(obj)

            item['timestamp_desc'] = 'Used Time'
            item['data_type'] = 'chrome:autofill:entry'
            item['source_long'] = 'Chrome Autofill'
            item['usage_count'] = item['count']
            item['field_name'] = item['name']

            item['message'] = '{}: {} (times used: {})'.format(
                item['field_name'], item['value'], item['usage_count'])

            del(item['name'], item['row_type'], item['count'], item['date_created'])
            return item

        if isinstance(obj, Chrome.BookmarkItem):
            item = HindsightEncoder.base_encoder(obj)

            item['timestamp_desc'] = 'Creation Time'
            item['data_type'] = 'chrome:bookmark:entry'
            item['source_long'] = 'Chrome Bookmarks'

            item['message'] = '{} ({}) bookmarked in folder "{}"'.format(
                item['name'], item['url'], item['parent_folder'])

            del(item['value'], item['row_type'], item['date_added'])
            return item

        if isinstance(obj, Chrome.BookmarkFolderItem):
            item = HindsightEncoder.base_encoder(obj)

            item['timestamp_desc'] = 'Creation Time'
            item['data_type'] = 'chrome:bookmark:folder'
            item['source_long'] = 'Chrome Bookmarks'

            item['message'] = '"{}" bookmark folder created in folder "{}"'.format(
                item['name'], item['parent_folder'])

            del(item['value'], item['row_type'], item['date_added'])
            return item

        if isinstance(obj, Chrome.LocalStorageItem):
            item = HindsightEncoder.base_encoder(obj)

            item['timestamp_desc'] = 'Not a time'
            item['data_type'] = 'chrome:local_storage:entry'
            item['source_long'] = 'Chrome LocalStorage'
            item['url'] = item['origin'][1:]

            item['message'] = 'key: {} value: {}'.format(
                item['key'], item['value'])

            del (item['row_type'])
            return item

        if isinstance(obj, Chrome.SessionStorageItem):
            item = HindsightEncoder.base_encoder(obj)

            item['timestamp_desc'] = 'Not a time'
            item['data_type'] = 'chrome:session_storage:entry'
            item['source_long'] = 'Chrome Session Storage'
            item['url'] = item['origin']

            item['message'] = 'key: {} value: {}'.format(
                item['key'], item['value'])

            del (item['row_type'])
            return item

        if isinstance(obj, Chrome.FileSystemItem):
            item = HindsightEncoder.base_encoder(obj)

            item['timestamp_desc'] = 'Not a time'
            item['data_type'] = 'chrome:file_system:entry'
            item['source_long'] = 'Chrome File System'
            item['url'] = item['origin']

            item['message'] = 'key: {} value: {}'.format(
                item['key'], item['value'])

            del (item['row_type'])
            return item

        if isinstance(obj, Chrome.LoginItem):
            item = HindsightEncoder.base_encoder(obj)

            item['timestamp_desc'] = 'Used Time'
            item['data_type'] = 'chrome:login_item:entry'
            item['source_long'] = 'Chrome Logins'
            item['usage_count'] = item['count']

            item['message'] = '{}: {} used on {} (total times used: {})'.format(
                item['name'], item['value'], item['url'], item['usage_count'])

            del(item['row_type'], item['count'], item['date_created'])
            return item

        if isinstance(obj, Chrome.PreferenceItem):
            item = HindsightEncoder.base_encoder(obj)

            item['timestamp_desc'] = 'Update Time'
            item['data_type'] = 'chrome:preferences:entry'
            item['source_long'] = 'Chrome Preferences'
            item['message'] = f'Updated preference: {item["key"]}: {item["value"]})'

            del(item['row_type'], item['name'])
            return item

        if isinstance(obj, Chrome.SiteSetting):
            item = HindsightEncoder.base_encoder(obj)

            if item['key'] == 'Status: Deleted':
                item['timestamp_desc'] = 'Not a time'
            else:
                item['timestamp_desc'] = 'Update Time'

            item['data_type'] = 'chrome:site_setting:entry'
            item['source_long'] = 'Chrome Site Settings'

            if item['key'] == 'Status: Deleted':
                item['message'] = 'Updated site setting (recovered deleted record)'
            else:
                item['message'] = f'Updated site setting: {item["key"]}: {item["value"]})'

            del(item['row_type'], item['name'])
            return item

        if isinstance(obj, CacheEntry):
            item = HindsightEncoder.base_encoder(obj)

            item['timestamp_desc'] = 'Last Visit Time'
            item['data_type'] = 'chrome:cache:entry'
            item['source_long'] = 'Chrome Cache'
            item['original_url'] = item['url']
            item['cache_type'] = item['row_type']
            item['cached_state'] = item['name']

            item['message'] = f'Original URL: {item["original_url"]}'

            del(item['row_type'], item['name'], item['timezone'])
            return item

class AnalysisSession(object):
    def __init__(
            self, input_path=None, profile_paths=None, cache_path=None, browser_type=None, available_input_types=None,
            version=None, display_version=None, output_name=None, log_path=None, no_copy=None, temp_dir=None,
            timezone=None, available_output_formats=None, selected_output_format=None, available_decrypts=None,
            selected_decrypts=None, parsed_artifacts=None, artifacts_display=None, artifacts_counts=None,
            parsed_storage=None, plugin_descriptions=None, selected_plugins=None, plugin_results=None,
            hindsight_version=None, preferences=None):
        self.input_path = input_path
        self.profile_paths = profile_paths
        self.cache_path = cache_path
        self.browser_type = browser_type
        self.available_input_types = available_input_types
        self.version = version
        self.display_version = display_version
        self.output_name = output_name
        self.log_path = log_path
        self.no_copy = no_copy
        self.temp_dir = temp_dir
        self.timezone = timezone
        self.available_output_formats = available_output_formats
        self.selected_output_format = selected_output_format
        self.available_decrypts = available_decrypts
        self.selected_decrypts = selected_decrypts
        self.parsed_artifacts = parsed_artifacts
        self.artifacts_display = artifacts_display
        self.artifacts_counts = artifacts_counts
        self.parsed_storage = parsed_storage
        self.plugin_descriptions = plugin_descriptions
        self.selected_plugins = selected_plugins
        self.plugin_results = plugin_results
        self.hindsight_version = hindsight_version
        self.preferences = preferences

        if self.version is None:
            self.version = []

        if self.available_input_types is None:
            self.available_input_types = ['Chrome', 'Brave']

        if self.parsed_artifacts is None:
            self.parsed_artifacts = []

        if self.artifacts_counts is None:
            self.artifacts_counts = {}

        if self.parsed_storage is None:
            self.parsed_storage = []

        if self.available_output_formats is None:
            self.available_output_formats = ['sqlite', 'jsonl']

        if self.available_decrypts is None:
            self.available_decrypts = {'windows': 0, 'mac': 0, 'linux': 0}

        if self.plugin_results is None:
            self.plugin_results = {}

        if self.preferences is None:
            self.preferences = []

        if __version__:
            self.hindsight_version = __version__

        # Try to import modules for different output formats, adding to self.available_output_format array if successful
        try:
            import xlsxwriter
            self.available_output_formats.append('xlsx')
        except ImportError:
            log.warning("Couldn't import module 'xlsxwriter'; XLSX output disabled.")

        # Set output name to default if not set by user
        if self.output_name is None:
            self.output_name = f'Hindsight Report ({time.strftime("%Y-%m-%dT%H-%M-%S")})'

        # Try to import modules for cookie decryption on different OSes.
        # Windows
        try:
            import win32crypt
            self.available_decrypts['windows'] = 1
        except ImportError:
            self.available_decrypts['windows'] = 0

        # Mac OS
        try:
            import keyring
            self.available_decrypts['mac'] = 1
        except ImportError:
            self.available_decrypts['mac'] = 0

        # Linux / Mac OS
        try:
            import Cryptodome.Cipher.AES
            import Cryptodome.Protocol.KDF
            self.available_decrypts['linux'] = 1
        except ImportError:
            self.available_decrypts['linux'] = 0
            self.available_decrypts['mac'] = 0

    @staticmethod
    def sum_dict_counts(dict1, dict2):
        """Combine two dicts by summing the values of shared keys"""
        for key, value in list(dict2.items()):
            # Case 1: dict2's value for key is a string (aka: it failed)
            if isinstance(value, str):
                #  The value should only be non-int if it's a Failed message
                if not value.startswith('Fail'):
                    raise ValueError('Unexpected status value')

                dict1[key] = dict1.setdefault(key, 0)

            # Case 2: dict1's value of key is a string (aka: it failed)
            elif isinstance(dict1.get(key), str):
                #  The value should only be non-int if it's a Failed message
                if not dict1.get(key).startswith('Fail'):
                    raise ValueError('Unexpected status value')

                dict1[key] = value

            # Case 3: dict2's value for key is an int, or doesn't exist.
            else:
                dict1[key] = dict1.setdefault(key, 0) + value
        return dict1

    def promote_object_to_analysis_session(self, item_name, item_value):
        if self.__dict__.get(item_name):
            self.__dict__[item_name]['data'].extend(item_value['data'])
            # TODO: add some checks around value of presentation. It *shouldn't* differ...
            self.__dict__[item_name]['presentation'] = item_value['presentation']

        else:
            setattr(self, item_name, item_value)

    @staticmethod
    def is_profile(base_path, existing_files, warn=False):
        """Log a warning message if any file in `required_files` is missing from
        `existing_files`. Return True if all required files are present.
        """
        is_profile = True
        for required_file in ['History']:
            # This approach (checking the file names) is naive but should work.
            if required_file not in existing_files or not os.path.isfile(os.path.join(base_path, required_file)):
                if warn:
                    log.warning("The profile directory {} does not contain the "
                                "file {}. Analysis may not be very useful."
                                .format(base_path, required_file))
                is_profile = False
        return is_profile

    def search_subdirs(self, base_path):
        """Recursively search a path for browser profiles"""
        found_profile_paths = []

        try:
            base_dir_listing = os.listdir(base_path)
        except Exception as e:
            log.warning(f'Unable to read directory; Exception: {e}')
            return found_profile_paths

        if self.is_profile(base_path, base_dir_listing):
            found_profile_paths.append(base_path)
        for item in base_dir_listing:
            item_path = os.path.join(base_path, item)
            if os.path.isdir(item_path) and not os.path.islink(item_path):
                profile_found_in_subdir = self.search_subdirs(item_path)
                if profile_found_in_subdir:
                    found_profile_paths.extend(profile_found_in_subdir)
        return found_profile_paths

    def find_browser_profiles(self, base_path):
        """Search a path for browser profiles (only Chromium-based at the moment)."""
        found_profile_paths = []
        base_dir_listing = os.listdir(base_path)

        # The 'History' SQLite file is kind of the minimum required for most
        # Chrome analysis. Warn if not present.
        if self.is_profile(base_path, base_dir_listing, warn=True):
            found_profile_paths.append(base_path)

        else:
            # Only search sub dirs if the current dir is not a Profile (Profiles are not nested).
            found_profile_paths.extend(self.search_subdirs(base_path))

        # If we did not find any valid Profiles, attempt to process the input
        # path as a Profile
        if not found_profile_paths:
            log.warning("No Profile paths found; processing input path as a Profile")
            found_profile_paths = [base_path]

        log.debug("Profile paths: " + str(found_profile_paths))
        return found_profile_paths

    def generate_display_version(self):
        self.version = sorted(self.version)
        if self.version is not None and len(self.version) >= 1:
            if self.version[0] != self.version[-1]:
                self.display_version = f'{self.version[0]}-{self.version[-1]}'
            else:
                self.display_version = self.version[0]
        else:
            self.display_version = "None"

    def run(self):
        if self.selected_output_format is None:
            self.selected_output_format = self.available_output_formats[-1]

        if 'pytz' in sys.modules:
            # If the timezone exists, and is a string, we need to convert it to a tzinfo object
            if self.timezone is not None and isinstance(self.timezone, str):
                try:
                    self.timezone = pytz.timezone(self.timezone)
                except pytz.exceptions.UnknownTimeZoneError:
                    print("Couldn't understand timezone; using UTC.")
                    self.timezone = pytz.timezone('UTC')

            elif self.timezone is None:
                self.timezone = pytz.timezone('UTC')
        else:
            self.timezone = None

        log.debug("Options: " + str(self.__dict__))

        # Analysis start time
        log.info("Starting analysis")

        log.info(f'Reading files from {self.input_path}')
        try:
            input_listing = os.listdir(self.input_path)
        except (PermissionError, OSError) as e:
            fail_message = f'Unable to read input directory; {e}'
            log.error(fail_message)
            self.fatal_error = fail_message
            return False
        log.debug("Input directory contents: " + str(input_listing))

        # Search input directory for browser profiles to analyze
        input_profiles = self.find_browser_profiles(self.input_path)
        log.info(f' - Found {len(input_profiles)} browser profile(s): {input_profiles}')
        self.profile_paths = input_profiles

        # Make sure the input is what we're expecting
        assert isinstance(self.profile_paths, list)
        assert len(self.profile_paths) >= 1

        for found_profile_path in self.profile_paths:

            if self.browser_type == "Chrome":
                browser_analysis = Chrome(found_profile_path, available_decrypts=self.available_decrypts,
                                          cache_path=self.cache_path, timezone=self.timezone,
                                          no_copy=self.no_copy, temp_dir=self.temp_dir)
                browser_analysis.process()
                self.parsed_artifacts.extend(browser_analysis.parsed_artifacts)
                self.parsed_storage.extend(browser_analysis.parsed_storage)
                self.artifacts_counts = self.sum_dict_counts(self.artifacts_counts, browser_analysis.artifacts_counts)
                self.artifacts_display = browser_analysis.artifacts_display
                self.version.extend(browser_analysis.version)
                self.display_version = browser_analysis.display_version
                self.preferences.extend(browser_analysis.preferences)

                for item in browser_analysis.__dict__:
                    if isinstance(browser_analysis.__dict__[item], dict):
                        try:
                            # If the browser_analysis attribute has 'presentation' and 'data' subkeys, promote from
                            if browser_analysis.__dict__[item].get('presentation') and \
                                    browser_analysis.__dict__[item].get('data'):
                                self.promote_object_to_analysis_session(item, browser_analysis.__dict__[item])
                        except Exception as e:
                            log.info(f'Exception occurred while analyzing {item} for analysis session promotion: {e}')

            elif self.browser_type == "Brave":
                browser_analysis = Brave(found_profile_path, timezone=self.timezone)
                browser_analysis.process()
                self.parsed_artifacts = browser_analysis.parsed_artifacts
                self.parsed_storage.extend(browser_analysis.parsed_storage)
                self.artifacts_counts = browser_analysis.artifacts_counts
                self.artifacts_display = browser_analysis.artifacts_display
                self.version = browser_analysis.version
                self.display_version = browser_analysis.display_version

                for item in browser_analysis.__dict__:
                    if isinstance(browser_analysis.__dict__[item], dict):
                        try:
                            # If the browser_analysis attribute has 'presentation' and 'data' subkeys, promote from
                            if browser_analysis.__dict__[item].get('presentation') and \
                                    browser_analysis.__dict__[item].get('data'):
                                self.promote_object_to_analysis_session(item, browser_analysis.__dict__[item])
                        except Exception as e:
                            log.info(f'Exception occurred while analyzing {item} for analysis session promotion: {e}')

        self.generate_display_version()
        return True

    def run_plugins(self):
        log.info("Selected plugins: " + str(self.selected_plugins))
        completed_plugins = []

        for plugin in self.selected_plugins:

            # First check built-in plugins that ship with Hindsight
            # log.info(" Built-in Plugins:")
            for standard_plugin in pyhindsight.plugins.__all__:
                # Check if the standard plugin is the selected_plugin we're looking for
                if standard_plugin == plugin:
                    # Check to see if we've already run this plugin (likely from a different path)
                    if plugin in completed_plugins:
                        log.info(" - Skipping '{}'; a plugin with that name has run already".format(plugin))
                        continue

                    log.info(" - Loading '{}' [standard plugin]".format(plugin))
                    try:
                        module = importlib.import_module("pyhindsight.plugins.{}".format(plugin))
                    except ImportError as e:
                        log.error(" - Error: {}".format(e))
                        print(format_plugin_output(plugin, "-unknown", 'import failed (see log)'))
                        continue
                    try:
                        log.info(" - Running '{}' plugin".format(module.friendlyName))
                        parsed_items = module.plugin(self)
                        print(format_plugin_output(module.friendlyName, module.version, parsed_items))
                        self.plugin_results[plugin] = [module.friendlyName, module.version, parsed_items]
                        log.info(" - Completed; {}".format(parsed_items))
                        completed_plugins.append(plugin)
                        break
                    except Exception as e:
                        print(format_plugin_output(module.friendlyName, module.version, 'failed'))
                        self.plugin_results[plugin] = [module.friendlyName, module.version, 'failed']
                        log.info(" - Failed; {}".format(e))

            for potential_path in sys.path:
                # If a subdirectory exists called 'plugins' at the current path, continue on
                potential_plugin_path = os.path.join(potential_path, 'plugins')
                if os.path.isdir(potential_plugin_path):
                    try:
                        # Insert the current plugin location to the system path, so we can import plugin modules by name
                        sys.path.insert(0, potential_plugin_path)

                        # Get list of available plugins and run them
                        plugin_listing = os.listdir(potential_plugin_path)

                        for custom_plugin in plugin_listing:
                            if custom_plugin[-3:] == ".py" and custom_plugin[0] != '_':
                                custom_plugin = custom_plugin.replace(".py", "")

                                if custom_plugin == plugin:
                                    # Check to see if we've already run this plugin (likely from a different path)
                                    if plugin in completed_plugins:
                                        log.info(f" - Skipping '{plugin}'; a plugin with that name has run already")
                                        continue

                                    log.debug(" - Loading '{}' [custom plugin]".format(plugin))
                                    try:
                                        module = __import__(plugin)
                                    except ImportError as e:
                                        log.error(" - Error: {}".format(e))
                                        print(format_plugin_output(plugin, "-unknown", 'import failed (see log)'))
                                        continue
                                    try:
                                        log.info(" - Running '{}' plugin".format(module.friendlyName))
                                        parsed_items = module.plugin(self)
                                        print(format_plugin_output(module.friendlyName, module.version, parsed_items))
                                        self.plugin_results[plugin] = [module.friendlyName, module.version, parsed_items]
                                        log.info(" - Completed; {}".format(parsed_items))
                                        completed_plugins.append(plugin)
                                    except Exception as e:
                                        print(format_plugin_output(module.friendlyName, module.version, 'failed'))
                                        self.plugin_results[plugin] = [module.friendlyName, module.version, 'failed']
                                        log.info(" - Failed; {}".format(e))
                    except Exception as e:
                        log.debug(' - Error loading plugins ({})'.format(e))
                        print('  - Error loading plugins')
                    finally:
                        # Remove the current plugin location from the system path, so we don't loop over it again
                        sys.path.remove(potential_plugin_path)

    def generate_excel(self, output_object):
        import xlsxwriter
        workbook = xlsxwriter.Workbook(output_object, {'in_memory': True})
        w = workbook.add_worksheet('Timeline')

        # Define cell formats
        title_header_format = workbook.add_format({'font_color': 'white', 'bg_color': 'gray', 'bold': 'true'})
        center_header_format = workbook.add_format(
            {'font_color': 'black', 'align': 'center', 'bg_color': 'gray', 'bold': 'true'})
        header_format = workbook.add_format({'font_color': 'black', 'bg_color': 'gray', 'bold': 'true'})
        black_type_format = workbook.add_format({'font_color': 'black', 'align': 'left'})
        black_date_format = workbook.add_format({'font_color': 'black', 'num_format': 'yyyy-mm-dd hh:mm:ss.000'})
        black_url_format = workbook.add_format({'font_color': 'black', 'align': 'left'})
        black_field_format = workbook.add_format({'font_color': 'black', 'align': 'left'})
        black_value_format = workbook.add_format({'font_color': 'black', 'align': 'left', 'num_format': '0'})
        black_flag_format = workbook.add_format({'font_color': 'black', 'align': 'center'})
        black_trans_format = workbook.add_format({'font_color': 'black', 'align': 'left'})
        gray_type_format = workbook.add_format({'font_color': 'gray', 'align': 'left'})
        gray_date_format = workbook.add_format({'font_color': 'gray', 'num_format': 'yyyy-mm-dd hh:mm:ss.000'})
        gray_url_format = workbook.add_format({'font_color': 'gray', 'align': 'left'})
        gray_field_format = workbook.add_format({'font_color': 'gray', 'align': 'left'})
        gray_value_format = workbook.add_format({'font_color': 'gray', 'align': 'left', 'num_format': '0'})
        gray_wrap_format = workbook.add_format({'font_color': 'gray', 'align': 'left', 'text_wrap': True})
        red_type_format = workbook.add_format({'font_color': 'red', 'align': 'left'})
        red_date_format = workbook.add_format({'font_color': 'red', 'num_format': 'yyyy-mm-dd hh:mm:ss.000'})
        red_url_format = workbook.add_format({'font_color': 'red', 'align': 'left'})
        red_field_format = workbook.add_format({'font_color': 'red', 'align': 'right'})
        red_value_format = workbook.add_format({'font_color': 'red', 'align': 'left', 'num_format': '0'})
        green_type_format = workbook.add_format({'font_color': 'green', 'align': 'left'})
        green_date_format = workbook.add_format({'font_color': 'green', 'num_format': 'yyyy-mm-dd hh:mm:ss.000'})
        green_url_format = workbook.add_format({'font_color': 'green', 'align': 'left'})
        green_field_format = workbook.add_format({'font_color': 'green', 'align': 'left'})
        green_value_format = workbook.add_format({'font_color': 'green', 'align': 'left'})
        blue_type_format = workbook.add_format({'font_color': 'blue', 'align': 'left'})
        blue_date_format = workbook.add_format({'font_color': 'blue', 'num_format': 'yyyy-mm-dd hh:mm:ss.000'})
        blue_url_format = workbook.add_format({'font_color': 'blue', 'align': 'left'})
        blue_field_format = workbook.add_format({'font_color': 'blue', 'align': 'left'})
        blue_value_format = workbook.add_format({'font_color': 'blue', 'align': 'left'})

        # Title bar
        w.merge_range('A1:H1', 'Hindsight Internet History Forensics (v%s)' % __version__, title_header_format)
        w.merge_range('I1:M1', 'URL Specific', center_header_format)
        w.merge_range('N1:P1', 'Download Specific', center_header_format)
        w.merge_range('Q1:R1', '', center_header_format)
        w.merge_range('S1:U1', 'Cache Specific', center_header_format)

        # Write column headers
        w.write(1, 0, 'Type', header_format)
        w.write(1, 1, 'Timestamp ({})'.format(self.timezone), header_format)
        w.write(1, 2, 'URL', header_format)
        w.write(1, 3, 'Title / Name / Status', header_format)
        w.write(1, 4, 'Data / Value / Path', header_format)
        w.write(1, 5, 'Interpretation', header_format)
        w.write(1, 6, 'Profile', header_format)
        w.write(1, 7, 'Source', header_format)
        w.write(1, 8, 'Duration', header_format)
        w.write(1, 9, 'Visit Count', header_format)
        w.write(1, 10, 'Typed Count', header_format)
        w.write(1, 11, 'URL Hidden', header_format)
        w.write(1, 12, 'Transition', header_format)
        w.write(1, 13, 'Interrupt Reason', header_format)
        w.write(1, 14, 'Danger Type', header_format)
        w.write(1, 15, 'Opened?', header_format)
        w.write(1, 16, 'ETag', header_format)
        w.write(1, 17, 'Last Modified', header_format)
        w.write(1, 18, 'Server Name', header_format)
        w.write(1, 19, 'Data Location [Offset]', header_format)
        w.write(1, 20, 'All HTTP Headers', header_format)

        # Set column widths
        w.set_column('A:A', 16)  # Type
        w.set_column('B:B', 21)  # Date
        w.set_column('C:C', 60)  # URL
        w.set_column('D:D', 25)  # Title / Name / Status
        w.set_column('E:E', 80)  # Data / Value / Path
        w.set_column('F:F', 60)  # Interpretation
        w.set_column('G:G', 12)  # Profile
        w.set_column('H:H', 10)  # Source

        # URL Specific
        w.set_column('I:I', 14)  # Visit Duration
        w.set_column('J:L', 6)   # Visit Count, Typed Count, Hidden
        w.set_column('M:M', 12)  # Transition

        # Download Specific
        w.set_column('N:N', 12)  # Interrupt Reason
        w.set_column('O:O', 24)  # Danger Type
        w.set_column('P:P', 12)  # Opened

        # Common between Downloads and Cache
        w.set_column('Q:Q', 12)  # ETag
        w.set_column('R:R', 27)  # Last Modified

        # Cache Specific
        w.set_column('S:S', 18)  # Server Name
        w.set_column('T:T', 27)  # Data Location
        w.set_column('U:U', 27)  # HTTP Headers

        # Start at the row after the headers, and begin writing out the items in parsed_artifacts
        row_number = 2
        for item in self.parsed_artifacts:
            try:
                if item.row_type.startswith("url"):
                    w.write_string(row_number, 0, item.row_type, black_type_format)  # record_type
                    w.write(row_number, 1, friendly_date(item.timestamp), black_date_format)  # date
                    w.write_string(row_number, 2, item.url, black_url_format)  # URL
                    w.write_string(row_number, 3, item.name, black_field_format)  # Title
                    w.write(row_number, 4, "", black_value_format)  # Indexed Content
                    w.write(row_number, 5, item.interpretation, black_value_format)  # Interpretation
                    w.write(row_number, 6, item.profile, black_type_format)  # Profile
                    w.write(row_number, 7, item.visit_source, black_type_format)  # Source
                    w.write(row_number, 8, item.visit_duration, black_flag_format)  # Duration
                    w.write(row_number, 9, item.visit_count, black_flag_format)  # Visit Count
                    w.write(row_number, 10, item.typed_count, black_flag_format)  # Typed Count
                    w.write(row_number, 11, item.hidden, black_flag_format)  # Hidden
                    w.write(row_number, 12, item.transition_friendly, black_trans_format)  # Transition

                elif item.row_type.startswith("media"):
                    w.write_string(row_number, 0, item.row_type, blue_type_format)  # record_type
                    w.write(row_number, 1, friendly_date(item.timestamp), blue_date_format)  # date
                    w.write_string(row_number, 2, item.url, blue_url_format)  # URL
                    w.write_string(row_number, 3, item.title, blue_field_format)  # Title
                    if item.source_title:
                        media_message = f'Watched{item.watch_time} on {item.source_title} '\
                                        f'(ending at {item.position}/{item.media_duration}) '\
                                        f'[has_video: {item.has_video}; has_audio: {item.has_audio}]'
                    else:
                        media_message = f'Watched{item.watch_time} ' \
                                        f'[has_video: {item.has_video}; has_audio: {item.has_audio}]'
                    w.write(row_number, 4, media_message, blue_value_format)
                    w.write(row_number, 5, item.interpretation, blue_value_format)  # Interpretation
                    w.write(row_number, 6, item.profile, blue_type_format)  # Profile

                elif item.row_type.startswith("autofill"):
                    w.write_string(row_number, 0, item.row_type, red_type_format)  # record_type
                    w.write(row_number, 1, friendly_date(item.timestamp), red_date_format)  # date
                    w.write_string(row_number, 3, item.name, red_field_format)  # autofill field
                    w.write_string(row_number, 4, item.value, red_value_format)  # autofill value
                    w.write(row_number, 6, item.profile, red_type_format)  # Profile

                elif item.row_type.startswith("download"):
                    w.write_string(row_number, 0, item.row_type, green_type_format)  # record_type
                    w.write(row_number, 1, friendly_date(item.timestamp), green_date_format)  # date
                    w.write_string(row_number, 2, item.url, green_url_format)  # download URL
                    w.write_string(row_number, 3, item.status_friendly, green_field_format)  # % complete
                    w.write_string(row_number, 4, item.value, green_value_format)  # download path
                    w.write_string(row_number, 5, "", green_field_format)  # Interpretation (chain?)
                    w.write(row_number, 6, item.profile, green_type_format)  # Profile
                    w.write(row_number, 13, item.interrupt_reason_friendly, green_value_format)  # interrupt reason
                    w.write(row_number, 14, item.danger_type_friendly, green_value_format)  # danger type
                    open_friendly = ""
                    if item.opened == 1:
                        open_friendly = 'Yes'
                    elif item.opened == 0:
                        open_friendly = 'No'
                    w.write_string(row_number, 15, open_friendly, green_value_format)  # opened
                    w.write(row_number, 16, item.etag, green_value_format)  # ETag
                    w.write(row_number, 17, item.last_modified, green_value_format)  # Last Modified

                elif item.row_type.startswith("bookmark folder"):
                    w.write_string(row_number, 0, item.row_type, red_type_format)  # record_type
                    w.write(row_number, 1, friendly_date(item.timestamp), red_date_format)  # date
                    w.write_string(row_number, 3, item.name, red_value_format)  # bookmark name
                    w.write_string(row_number, 4, item.value, red_value_format)  # bookmark folder
                    w.write(row_number, 6, item.profile, red_value_format)  # Profile

                elif item.row_type.startswith("bookmark"):
                    w.write_string(row_number, 0, item.row_type, red_type_format)  # record_type
                    w.write(row_number, 1, friendly_date(item.timestamp), red_date_format)  # date
                    w.write_string(row_number, 2, item.url, red_url_format)  # URL
                    w.write_string(row_number, 3, item.name, red_value_format)  # bookmark name
                    w.write_string(row_number, 4, item.value, red_value_format)  # bookmark folder
                    w.write(row_number, 6, item.profile, red_value_format)  # Profile

                elif item.row_type.startswith("cookie"):
                    w.write_string(row_number, 0, item.row_type, gray_type_format)  # record_type
                    w.write(row_number, 1, friendly_date(item.timestamp), gray_date_format)  # date
                    w.write_string(row_number, 2, item.url, gray_url_format)  # URL
                    w.write_string(row_number, 3, item.name, gray_field_format)  # cookie name
                    w.write_string(row_number, 4, item.value, gray_value_format)  # cookie value
                    w.write(row_number, 5, item.interpretation, gray_value_format)  # cookie interpretation
                    w.write(row_number, 6, item.profile, gray_value_format)  # Profile

                elif item.row_type.startswith("cache"):
                    w.write_string(row_number, 0, item.row_type, gray_type_format)  # record_type
                    w.write(row_number, 1, friendly_date(item.timestamp), gray_date_format)  # date
                    try:
                        w.write_string(row_number, 2, item.url, gray_url_format)  # URL
                    except Exception as e:
                        print(e, item.url, item.location)
                    w.write_string(row_number, 3, str(item.name), gray_field_format)  # status // Normal (data cached)
                    w.write_string(row_number, 4, item.value, gray_value_format)  # type (size) // image/jpeg (35 bytes)
                    w.write(row_number, 5, item.interpretation, gray_value_format)  # cookie interpretation
                    w.write(row_number, 6, item.profile, gray_value_format)  # Profile
                    w.write(row_number, 16, item.etag, gray_value_format)  # ETag
                    w.write(row_number, 17, item.last_modified, gray_value_format)  # Last Modified
                    w.write(row_number, 18, item.server_name, gray_value_format)  # Server name
                    w.write(row_number, 19, item.location, gray_value_format)  # data location // data_2 [1542523]
                    w.write(row_number, 20, item.http_headers_str, gray_value_format)  # headers

                elif item.row_type.startswith("local storage"):
                    w.write_string(row_number, 0, item.row_type, gray_type_format)  # record_type
                    w.write(row_number, 1, friendly_date(item.timestamp), gray_date_format)  # date
                    w.write_string(row_number, 2, item.url, gray_url_format)  # URL
                    w.write_string(row_number, 3, item.name, gray_field_format)  # cookie name
                    w.write_string(row_number, 4, item.value, gray_value_format)  # cookie value
                    w.write(row_number, 5, item.interpretation, gray_value_format)  # cookie interpretation
                    w.write(row_number, 6, item.profile, gray_value_format)  # Profile

                elif item.row_type.startswith("login"):
                    w.write_string(row_number, 0, item.row_type, red_type_format)  # record_type
                    w.write(row_number, 1, friendly_date(item.timestamp), red_date_format)  # date
                    w.write_string(row_number, 2, item.url, red_url_format)  # URL
                    w.write_string(row_number, 3, item.name, red_field_format)  # form field name
                    w.write_string(row_number, 4, item.value, red_value_format)  # username or pw value
                    w.write_string(row_number, 5, item.interpretation, red_value_format)  # interpretation
                    w.write(row_number, 6, item.profile, red_value_format)  # Profile

                elif item.row_type.startswith("preference"):
                    w.write_string(row_number, 0, item.row_type, blue_type_format)  # record_type
                    w.write(row_number, 1, friendly_date(item.timestamp), blue_date_format)  # date
                    w.write_string(row_number, 2, item.url, blue_url_format)  # URL
                    w.write_string(row_number, 3, item.name, blue_field_format)  # form field name
                    w.write_string(row_number, 4, item.value, blue_value_format)  # username or pw value
                    w.write(row_number, 5, item.interpretation, blue_value_format)  # interpretation
                    w.write(row_number, 6, item.profile, blue_value_format)  # Profile

                elif item.row_type.startswith("site setting"):
                    w.write_string(row_number, 0, item.row_type, blue_type_format)  # record_type
                    w.write(row_number, 1, friendly_date(item.timestamp), blue_date_format)  # date
                    w.write_string(row_number, 2, item.url, blue_url_format)  # URL
                    w.write_string(row_number, 3, item.name, blue_field_format)  # form field name
                    w.write_string(row_number, 4, item.value, blue_value_format)  # username or pw value
                    w.write(row_number, 5, item.interpretation, blue_value_format)  # interpretation
                    w.write(row_number, 6, item.profile, blue_value_format)  # Profile

                if friendly_date(item.timestamp) < '1970-01-02':
                    w.set_row(row_number, options={'hidden': True})

            except Exception as e:
                log.error(f'Failed to write row to XLSX: {e}')

            row_number += 1

        # Formatting
        w.freeze_panes(2, 0)  # Freeze top row
        w.autofilter(1, 0, row_number, 19)  # Add autofilter
        w.filter_column('B', 'Timestamp > 1970-01-02')

        s = workbook.add_worksheet('Storage')
        # Title bar
        s.merge_range('A1:G1', f'Hindsight Internet History Forensics (v{__version__})', title_header_format)
        s.merge_range('H1:J1', 'Backing Database Specific', center_header_format)
        s.merge_range('K1:M1', 'FileSystem Specific', center_header_format)

        # Write column headers
        s.write(1, 0, 'Type', header_format)
        s.write(1, 1, 'Origin', header_format)
        s.write(1, 2, 'Key', header_format)
        s.write(1, 3, 'Value', header_format)
        s.write(1, 4, 'Modification Time ({})'.format(self.timezone), header_format)
        s.write(1, 5, 'Interpretation', header_format)
        s.write(1, 6, 'Profile', header_format)
        s.write(1, 7, 'Source Path', header_format)
        s.write(1, 8, 'Sequence', header_format)
        s.write(1, 9, 'State', header_format)
        s.write(1, 10, 'File Exists?', header_format)
        s.write(1, 11, 'File Size (bytes)', header_format)
        s.write(1, 12, 'File Type (Confidence %)', header_format)

        # Set column widths
        s.set_column('A:A', 16)  # Type
        s.set_column('B:B', 30)  # Origin
        s.set_column('C:C', 35)  # Key
        s.set_column('D:D', 60)  # Value
        s.set_column('E:E', 16)  # Mod Time
        s.set_column('F:F', 50)  # Interpretation
        s.set_column('G:G', 50)  # Profile
        s.set_column('H:H', 50)  # Source Path
        s.set_column('I:I', 8)  # Seq
        s.set_column('J:J', 8)  # State
        s.set_column('K:K', 8)  # Exists
        s.set_column('L:L', 16)  # Size
        s.set_column('M:M', 25)  # Type

        # Start at the row after the headers, and begin writing out the items in parsed_artifacts
        row_number = 2
        for item in self.parsed_storage:
            try:
                if item.row_type.startswith("file system"):
                    s.write_string(row_number, 0, item.row_type, black_type_format)
                    s.write_string(row_number, 1, item.origin, black_url_format)
                    s.write_string(row_number, 2, item.key, black_field_format)
                    s.write_string(row_number, 3, item.value, black_value_format)
                    s.write(row_number, 4, friendly_date(item.last_modified), black_date_format)
                    s.write(row_number, 5, item.interpretation, black_value_format)
                    s.write(row_number, 6, item.profile, black_value_format)
                    s.write(row_number, 7, item.source_path, black_value_format)
                    s.write_number(row_number, 8, item.seq, black_value_format)
                    s.write_string(row_number, 9, item.state, black_value_format)
                    s.write(row_number, 10, item.file_exists, black_value_format)
                    s.write(row_number, 11, item.file_size, black_value_format)
                    s.write(row_number, 12, item.magic_results, black_value_format)

                elif item.row_type.startswith(("local storage", "session storage")):
                    s.write_string(row_number, 0, item.row_type, black_type_format)
                    s.write_string(row_number, 1, item.origin, black_url_format)
                    s.write_string(row_number, 2, item.key, black_field_format)
                    s.write_string(row_number, 3, item.value, black_value_format)
                    s.write(row_number, 4, friendly_date(item.last_modified), black_date_format)
                    s.write(row_number, 5, item.interpretation, black_value_format)
                    s.write(row_number, 6, item.profile, black_value_format)
                    s.write(row_number, 7, item.source_path, black_value_format)
                    s.write_number(row_number, 8, item.seq, black_value_format)
                    s.write_string(row_number, 9, item.state, black_value_format)

            except Exception as e:
                log.error(f'Failed to write row to XLSX: {e}')

            row_number += 1

        # Formatting
        s.freeze_panes(2, 0)  # Freeze top row
        s.autofilter(1, 0, row_number, 12)  # Add autofilter

        for item in self.__dict__:
            try:
                if self.__dict__[item]['presentation'] and self.__dict__[item]['data']:
                    d = self.__dict__[item]
                    # TODO: try/except name exists
                    p = workbook.add_worksheet(d['presentation']['title'])
                    title = d['presentation']['title']
                    if 'version' in d['presentation']:
                        title += " (v{})".format(d['presentation']['version'])
                    p.merge_range(0, 0, 0, len(d['presentation']['columns']) - 1, f"{title}", title_header_format)
                    for counter, column in enumerate(d['presentation']['columns']):
                        # print column
                        p.write(1, counter, column['display_name'], header_format)
                        if 'display_width' in column:
                            p.set_column(counter, counter, column['display_width'])

                    for row_count, row in enumerate(d['data'], start=2):
                        if not isinstance(row, dict):
                            for column_count, column in enumerate(d['presentation']['columns']):
                                p.write(row_count, column_count, row.__dict__[column['data_name']], black_type_format)
                        else:
                            for column_count, column in enumerate(d['presentation']['columns']):
                                p.write(row_count, column_count, row[column['data_name']], black_type_format)

                    # Formatting
                    p.freeze_panes(2, 0)  # Freeze top row
                    p.autofilter(1, 0, len(d['data']) + 2, len(d['presentation']['columns']) - 1)  # Add autofilter

            except:
                pass

        # TODO: combine this with above function
        for item in self.__dict__.get('preferences'):
            try:
                if item['presentation'] and item['data']:
                    d = item
                    # TODO: try/except name exists
                    p = workbook.add_worksheet(d['presentation']['title'][:31])
                    title = d['presentation']['title']
                    if 'version' in d['presentation']:
                        title += " (v{})".format(d['presentation']['version'])
                    p.merge_range(0, 0, 0, len(d['presentation']['columns']) - 1, f"{title}", title_header_format)
                    for counter, column in enumerate(d['presentation']['columns']):
                        p.write(1, counter, column['display_name'], header_format)
                        if 'display_width' in column:
                            p.set_column(counter, counter, column['display_width'])

                    for row_count, row in enumerate(d['data'], start=2):
                        if not isinstance(row, dict):
                            for column_count, column in enumerate(d['presentation']['columns']):
                                p.write(row_count, column_count, row.__dict__[column['data_name']], black_type_format)
                        else:
                            for column_count, column in enumerate(d['presentation']['columns']):
                                p.write(row_count, column_count, row[column['data_name']], black_type_format)

                    # Formatting
                    p.freeze_panes(2, 0)  # Freeze top row
                    p.autofilter(1, 0, len(d['data']) + 2, len(d['presentation']['columns']) - 1)  # Add autofilter

            except Exception as e:
                log.warning("Exception occurred while writing Preferences page: {}".format(e))
                pass

        workbook.close()

    def generate_sqlite(self, output_file_path='.temp_db'):

        output_db = sqlite3.connect(output_file_path)
        output_db.text_factory = lambda x: str(x, 'utf-8', 'ignore')

        with output_db:
            c = output_db.cursor()
            c.execute(
                'CREATE TABLE timeline(type TEXT, timestamp TEXT, url TEXT, title TEXT, value TEXT, '
                'interpretation TEXT, profile TEXT, source TEXT, visit_duration TEXT, visit_count INT, '
                'typed_count INT, url_hidden INT, transition TEXT, interrupt_reason TEXT, danger_type TEXT, '
                'opened INT, etag TEXT, last_modified TEXT, server_name TEXT, data_location TEXT, http_headers TEXT)')

            c.execute(
                'CREATE TABLE storage(type TEXT, origin TEXT, key TEXT, value TEXT, seq INT, state TEXT, '
                'modification_time TEXT, interpretation TEXT, profile TEXT, source_path TEXT, '
                'file_exists BOOL, file_size INT, magic_results TEXT)')

            c.execute(
                'CREATE TABLE installed_extensions(name TEXT, description TEXT, version TEXT, app_id TEXT, '
                'profile TEXT)')

            for item in self.parsed_artifacts:
                if item.row_type.startswith('url'):
                    c.execute(
                        'INSERT INTO timeline (type, timestamp, url, title, interpretation, profile, source, '
                        'visit_duration, visit_count, typed_count, url_hidden, transition) '
                        'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
                        (item.row_type, friendly_date(item.timestamp), item.url, item.name, item.interpretation,
                         item.profile, item.visit_source, item.visit_duration, item.visit_count, item.typed_count,
                         item.hidden, item.transition_friendly))

                elif item.row_type.startswith('media'):
                    if item.source_title:
                        media_message = f'Watched{item.watch_time} on {item.source_title} '\
                                        f'(ending at {item.position}/{item.media_duration}) '\
                                        f'[has_video: {item.has_video}; has_audio: {item.has_audio}]'
                    else:
                        media_message = f'Watched{item.watch_time} '\
                                        f'[has_video: {item.has_video}; has_audio: {item.has_audio}]'
                    c.execute(
                        'INSERT INTO timeline (type, timestamp, url, title, value, interpretation, profile) '
                        'VALUES (?, ?, ?, ?, ?, ?, ?)',
                        (item.row_type, friendly_date(item.timestamp), item.url, item.title,
                         media_message, item.interpretation, item.profile))

                elif item.row_type.startswith('autofill'):
                    c.execute(
                        'INSERT INTO timeline (type, timestamp, title, value, interpretation, profile) '
                        'VALUES (?, ?, ?, ?, ?, ?)',
                        (item.row_type, friendly_date(item.timestamp), item.name, item.value, item.interpretation,
                         item.profile))

                elif item.row_type.startswith('download'):
                    c.execute(
                        'INSERT INTO timeline (type, timestamp, url, title, value, interpretation, profile, '
                        'interrupt_reason, danger_type, opened, etag, last_modified) '
                        'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
                        (item.row_type, friendly_date(item.timestamp), item.url, item.status_friendly, item.value,
                         item.interpretation, item.profile, item.interrupt_reason_friendly, item.danger_type_friendly,
                         item.opened, item.etag, item.last_modified))

                elif item.row_type.startswith('bookmark folder'):
                    c.execute(
                        'INSERT INTO timeline (type, timestamp, title, value, interpretation, profile) '
                        'VALUES (?, ?, ?, ?, ?, ?)',
                        (item.row_type, friendly_date(item.timestamp), item.name, item.value,
                         item.interpretation, item.profile))

                elif item.row_type.startswith('bookmark'):
                    c.execute(
                        'INSERT INTO timeline (type, timestamp, url, title, value, interpretation, profile) '
                        'VALUES (?, ?, ?, ?, ?, ?, ?)',
                        (item.row_type, friendly_date(item.timestamp), item.url, item.name, item.value,
                         item.interpretation, item.profile))

                elif item.row_type.startswith('cookie'):
                    c.execute(
                        'INSERT INTO timeline (type, timestamp, url, title, value, interpretation, profile) '
                        'VALUES (?, ?, ?, ?, ?, ?, ?)',
                        (item.row_type, friendly_date(item.timestamp), item.url, item.name, item.value,
                         item.interpretation, item.profile))

                elif item.row_type.startswith('local storage'):
                    c.execute(
                        'INSERT INTO timeline (type, timestamp, url, title, value, interpretation, profile) '
                        'VALUES (?, ?, ?, ?, ?, ?, ?)',
                        (item.row_type, friendly_date(item.timestamp), item.url, item.name, item.value,
                         item.interpretation, item.profile))

                elif item.row_type.startswith('cache'):
                    c.execute(
                        'INSERT INTO timeline (type, timestamp, url, title, value, interpretation, profile, '
                        'etag, last_modified, server_name, data_location)'
                        'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
                        (item.row_type, friendly_date(item.timestamp), item.url, str(item.name), item.value,
                         item.interpretation, item.profile, item.etag, item.last_modified, item.server_name,
                         item.location))

                elif item.row_type.startswith('login'):
                    c.execute(
                        'INSERT INTO timeline (type, timestamp, url, title, value, interpretation, profile) '
                        'VALUES (?, ?, ?, ?, ?, ?, ?)',
                        (item.row_type, friendly_date(item.timestamp), item.url, item.name, item.value,
                         item.interpretation, item.profile))

                elif item.row_type.startswith('preference'):
                    c.execute(
                        'INSERT INTO timeline (type, timestamp, url, title, value, interpretation, profile) '
                        'VALUES (?, ?, ?, ?, ?, ?, ?)',
                        (item.row_type, friendly_date(item.timestamp), item.url, item.name, item.value,
                         item.interpretation, item.profile))

                elif item.row_type.startswith('site setting'):
                    c.execute(
                        'INSERT INTO timeline (type, timestamp, url, title, value, interpretation, profile) '
                        'VALUES (?, ?, ?, ?, ?, ?, ?)',
                        (item.row_type, friendly_date(item.timestamp), item.url, item.name, item.value,
                         item.interpretation, item.profile))

            for item in self.parsed_storage:
                if item.row_type.startswith('local'):
                    c.execute(
                        'INSERT INTO storage (type, origin, key, value, modification_time, '
                        'interpretation, profile, source_path, seq, state) '
                        'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
                        (item.row_type, item.origin, item.key, item.value, item.last_modified,
                         item.interpretation, item.profile, item.source_path, item.seq, item.state))

                if item.row_type.startswith('session'):
                    c.execute(
                        'INSERT INTO storage (type, origin, key, value, '
                        'interpretation, profile, source_path, seq, state) '
                        'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
                        (item.row_type, item.origin, item.key, item.value, item.interpretation, item.profile,
                         item.source_path, item.seq, item.state))

                if item.row_type.startswith('file system'):
                    c.execute(
                        'INSERT INTO storage (type, origin, key, value, modification_time, '
                        'interpretation, profile, source_path, seq, state, file_exists, file_size, '
                        'magic_results) '
                        'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
                        (item.row_type, item.origin, item.key, item.value, item.last_modified,
                         item.interpretation, item.profile, item.source_path, item.seq, item.state,
                         item.file_exists, item.file_size, item.magic_results))

            if self.__dict__.get('installed_extensions'):
                for extension in self.installed_extensions['data']:
                    c.execute(
                        'INSERT INTO installed_extensions (name, description, version, app_id, profile) '
                        'VALUES (?, ?, ?, ?, ?)',
                        (extension.name, extension.description, extension.version, extension.app_id, extension.profile))

    def generate_jsonl(self, output_file):
        with open(output_file, mode='w') as jsonl:
            for parsed_artifact in self.parsed_artifacts:
                parsed_artifact_json = json.dumps(parsed_artifact, cls=HindsightEncoder)
                jsonl.write(parsed_artifact_json)
                jsonl.write('\n')
            for parsed_storage in self.parsed_storage:
                parsed_storage_json = json.dumps(parsed_storage, cls=HindsightEncoder)
                jsonl.write(parsed_storage_json)
                jsonl.write('\n')


from code_challenges.trees.binary_search_tree import BinaryTree
from code_challenges.hashtable.hashtable import HashTable

def tree_intersection(btone,bttwo):
    intersection = []
    ht = HashTable()

if __name__ == "__main__":
    hashed = HashTable()
    print(hashed.hash('8'))
    print(hashed.hash('8'))


"""Restaurant rating lister."""
import random

# put your code here
the_file = open("scores.txt")

scores_dictionary = {}

for line in the_file:
    line = line.rstrip()

    element = line.split(":")
    scores_dictionary[element[0]] = int(element[1])
# user input for new restaurant and score to be added to dictionary

restaurant_name = input("> ").title()

while True:
    try:
        restaurant_rating = int(input("Please enter a score for new restaurant: "))
        break
    except ValueError:
        print("uh oh!  That was no valid number.  Try again...")

while restaurant_rating not in range(1,6):
    print("Please pick number from 1 to 5")
    while True:
        try:
            restaurant_rating = int(input("Please enter a score for new restaurant: "))
            break
        except ValueError:
            print("uh oh!  That was no valid number.  Try again...")

scores_dictionary[restaurant_name] = restaurant_rating
alpha_list = scores_dictionary.keys()
alpha_list_sorted = sorted(alpha_list)
# for store in alpha_list_sorted:
#     print(f'{store} is rated at {scores_dictionary[store]}')

#select random restaurant from dictionary and prompt suer to change score
random_restaurant = random.choice(list(scores_dictionary.keys()))
# print(f'{random_restaurant} has a score of {scores_dictionary[random_restaurant]}')

while True:
    try:
        new_rating = int(input("Please enter a number: "))
        break
    except ValueError:
        print("Oops!  That was no valid number.  Try again...")

while new_rating not in range(1, 6):
    print("Please pick number from 1 to 5")
    while True:
        try:
            new_rating = int(input("Please enter a number: "))
            break
        except ValueError:
            print("Oops!  That was no valid number.  Try again...")

scores_dictionary[random_restaurant] = new_rating
print(f'{random_restaurant} has a score of {scores_dictionary[random_restaurant]}')


"""
DashInsert 함수는 숫자로 구성된 문자열을 입력받은 뒤,
문자열 내에서 홀수가 연속되면 두 수 사이에 - 를 추가하고,
짝수가 연속되면 * 를 추가하는 기능을 갖고 있다.
(예, 454 => 454, 4546793 => 454*67-9-3) DashInsert 함수를 완성하자.
"454*67-9-3"
입력 - 화면에서 숫자로 된 문자열을 입력받는다.
"""
s = input()
result = []
for i in range(len(s)):
    if i+1 < len(s):
        if int(s[i]) % 2 == 0 and int(s[i+1]) % 2 == 0:
            result.append(s[i] + "*")
        elif int(s[i]) % 2 != 0 and int(s[i+1]) % 2 != 0:
            result.append(s[i] + "-")
        else:
            result.append(s[i])
print("".join(result) + s[-1])

# s = "4546793"  # 454*67-9-3
# s = "477734702349"  # 47-7-7-3470*2349


#! /usr/local/bin/python3
"""
    input the filename you want to read

    try catch else the open() function

"""
import pdb

def readFile():
    filename = input("input the filename you want to read:")
    print("\n")

    try:

        fileobj = open(filename,"r")
    except IOError as  e:
        print("*** can not open ", e)
    else:
        lines = [line.strip() for line in fileobj]
        for line in lines:
            print(line,)
        fileobj.close()

if __name__ =="__main__":
    readFile()


def factorial():
    num = int(input("Enter number: "))
    answer = 1

    #Error Invalid Input
    if num <1:
        print("Invalid number")
        return

    for i in range (1, num +1 ):
        answer *= i

    print(f"{num} ! = {answer}")

def power():
    base = int(input("Enter base: "))
    exp = int(input("Enter exponent:"))
    answer= 1

    if base < 1 or exp < 1:
        print("Invalid input")
        return

    # loop through and calculate ansr, then display it
    for i in range (exp):
        answer *=base

    print(f"{base}^{exp} = {answer}")

power()
def sum():
    num=int(input("Enter number: "))
    answer= 0

    for i in range(1, num + 1):
        answer = 0
    print(f" Sum of 1... {num} = {answer}")
# program
print("Welcome to Math!")
while True:
    command = input("Compute (F)actorial, (S)um, (P)ower, or (Q)uit: ").strip().lower()

    if command =="q":
        break
    elif command =="f":
        factorial()
    elif command == "p":
        power()
    elif command =="s":
        sum()
    else:lower
    print("Invalid input")

print ("Goodbye")


import shutil
import tempfile

from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.cache import cache
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import Client, TestCase, override_settings
from django.urls import reverse
from django import forms

from ..models import Group, Post, Follow

TEMP_MEDIA_ROOT = tempfile.mkdtemp(dir=settings.BASE_DIR)

User = get_user_model()

@override_settings(MEDIA_ROOT=TEMP_MEDIA_ROOT)
class ViewTests(TestCase):
    @classmethod
    def setUpClass(cls):
        super().setUpClass()

        cls.USER_NAME = 'test_user'
        cls.GROUP_TITLE = 'Тестовая группа'
        cls.GROUP_SLUG = 'test_group_slug'
        cls.GROUP_2_SLUG = 'test_group_slug_2'

        cls.POST_TEXT = 'Тестовый пост'

        cls.user = User.objects.create_user(username=cls.USER_NAME)
        cls.followed_user = User.objects.create_user(username='follower')

        cls.group = Group.objects.create(
            title='Тестовая группа',
            slug=cls.GROUP_SLUG,
            description='Тестовое описание',
        )
        cls.group2 = Group.objects.create(
            title='Тестовая группа 2',
            slug=cls.GROUP_2_SLUG,
            description='Тестовое описание 2',
        )

        cls.small_gif = (
            b'\x47\x49\x46\x38\x39\x61\x02\x00'
            b'\x01\x00\x80\x00\x00\x00\x00\x00'
            b'\xFF\xFF\xFF\x21\xF9\x04\x00\x00'
            b'\x00\x00\x00\x2C\x00\x00\x00\x00'
            b'\x02\x00\x01\x00\x00\x02\x02\x0C'
            b'\x0A\x00\x3B'
        )
        cls.uploaded = SimpleUploadedFile(
            name='small.gif',
            content=cls.small_gif,
            content_type='image/gif'
        )

        cls.post = Post.objects.create(
            author=cls.user,
            text=cls.POST_TEXT,
            group=cls.group,
            image=SimpleUploadedFile(
                name='small.gif',
                content=cls.small_gif,
                content_type='image/gif'
            )
        )
        cls.image = cls.post.image
        cls.post_id = cls.post.id

        cls.follow = Follow.objects.create(
            user=cls.followed_user,
            author=cls.user,
        )

    @classmethod
    def tearDownClass(cls):
        super().tearDownClass()
        shutil.rmtree(TEMP_MEDIA_ROOT, ignore_errors=True)

    def setUp(self):
        self.authorized_client = Client()
        self.authorized_client.force_login(self.user)

    def post_obj_test_correct_context(self, post_obj):
        self.assertEqual(post_obj.pk, self.post_id)
        self.assertEqual(post_obj.author, self.user)
        self.assertEqual(post_obj.text, self.POST_TEXT)
        self.assertEqual(post_obj.group, self.group)
        self.assertEqual(post_obj.image, self.image)

    def test_pages_uses_correct_template(self):
        templates_pages_names = {
            reverse('posts:index'): 'posts/index.html',
            reverse(
                'posts:group_list',
                kwargs={'slug': self.GROUP_SLUG}
            ): 'posts/group_list.html',
            reverse(
                'posts:profile',
                kwargs={'username': self.USER_NAME}
            ): 'posts/profile.html',
            reverse(
                'posts:post_detail',
                kwargs={'post_id': self.post_id}
            ): 'posts/post_detail.html',
            reverse(
                'posts:post_edit',
                kwargs={'post_id': self.post_id}
            ): 'posts/create_post.html',
            reverse('posts:post_create'): 'posts/create_post.html',
        }
        for reverse_name, template in templates_pages_names.items():
            with self.subTest(reverse_name=reverse_name):
                response = self.authorized_client.get(reverse_name)
                self.assertTemplateUsed(response, template)

    def test_index_show_correct_context(self):
        response = self.authorized_client.get(reverse('posts:index'))
        self.assertEqual(
            response.context.get('title'),
            'Последние обновления на сайте'
        )
        self.assertEqual(response.context.get('index'), True)
        self.post_obj_test_correct_context(response.context.get('page_obj')[0])

    def test_group_show_correct_context(self):
        response = self.authorized_client.get(
            reverse('posts:group_list', kwargs={'slug': self.GROUP_SLUG})
        )

        self.assertEqual(
            response.context.get('title'),
            f'Записи сообщества "{self.GROUP_TITLE}"'
        )
        self.assertEqual(response.context.get('group'), self.group)
        self.post_obj_test_correct_context(response.context.get('page_obj')[0])

    def test_group_show_own_posts(self):
        """ Проверка, что на страницу группы без постов не попали посты """
        response = self.authorized_client.get(
            reverse('posts:group_list', kwargs={'slug': self.GROUP_2_SLUG})
        )

        self.assertEqual(len(response.context.get('page_obj')), 0)

    def test_profile_show_correct_context(self):
        response = self.authorized_client.get(
            reverse('posts:profile', kwargs={'username': self.user.username})
        )

        self.assertEqual(response.context.get('author'), self.user)
        self.assertEqual(response.context.get('author_posts_count'), 1)
        self.post_obj_test_correct_context(response.context.get('page_obj')[0])

    def test_post_detail_show_correct_context(self):
        response = self.authorized_client.get(
            reverse('posts:post_detail', kwargs={'post_id': self.post_id})
        )

        self.post_obj_test_correct_context(response.context.get('post'))
        self.assertEqual(response.context.get('author_posts_count'), 1)

    def test_post_edit_show_correct_context(self):
        response = self.authorized_client.get(
            reverse('posts:post_edit', kwargs={'post_id': self.post_id})
        )

        form_fields = {
            'text': forms.fields.CharField,
            'group': forms.fields.ChoiceField,
        }

        for value, expected in form_fields.items():
            with self.subTest(value=value):
                form_field = response.context.get('form').fields.get(value)
                self.assertIsInstance(form_field, expected)

    def test_post_create_show_correct_context(self):
        response = self.authorized_client.get(
            reverse('posts:post_create')
        )

        form_fields = {
            'text': forms.fields.CharField,
            'group': forms.fields.ChoiceField,
        }

        for value, expected in form_fields.items():
            with self.subTest(value=value):
                form_field = response.context.get('form').fields.get(value)
                self.assertIsInstance(form_field, expected)

    def test_follow_index_follower_client(self):
        follower_client = Client()
        follower_client.force_login(self.followed_user)
        response = follower_client.get(reverse(
            'posts:follow_index'
        ))
        follower_posts_count = len(response.context['page_obj'])
        self.assertEqual(follower_posts_count, 1)

    def test_follow_index_not_follower_client(self):
        response = self.authorized_client.get(reverse(
            'posts:follow_index'
        ))
        follower_posts_count = len(response.context['page_obj'])
        self.assertEqual(follower_posts_count, 0)

    def test_follow(self):
        followed_user = User.objects.create_user(username='follow_user')
        followed_client = Client()
        followed_client.force_login(followed_user)

        follow_count = Follow.objects.count()

        response = followed_client.post(
            reverse('posts:profile_follow', kwargs={'username': self.user}),
        )
        self.assertRedirects(
            response,
            reverse('posts:profile', kwargs={'username': self.user})
        )
        self.assertEqual(Follow.objects.count(), follow_count + 1)
        self.assertEqual(Follow.objects.last().user, followed_user)
        self.assertEqual(Follow.objects.last().author, self.user)

    def test_unfollow(self):
        followed_user = User.objects.create_user(username='unfollow_user')
        followed_client = Client()
        followed_client.force_login(followed_user)

        follow = Follow.objects.create(
            user=followed_user,
            author=self.user,
        )
        follow_id = follow.id
        follow_count = Follow.objects.count()

        response = followed_client.post(
            reverse('posts:profile_unfollow', kwargs={'username': self.user}),
        )
        self.assertRedirects(
            response,
            reverse('posts:profile', kwargs={'username': self.user})
        )

        self.assertEqual(Follow.objects.count(), follow_count - 1)
        self.assertFalse(Follow.objects.filter(pk=follow_id).exists())

class CacheTests(TestCase):
    @classmethod
    def setUpClass(cls):
        super().setUpClass()

        cls.USER_NAME = 'test_user'
        cls.GROUP_TITLE = 'Тестовая группа'
        cls.GROUP_SLUG = 'test_group_slug'
        cls.POST_TEXT = 'Тестовый пост'

        cls.user = User.objects.create_user(username=cls.USER_NAME)

        cls.group = Group.objects.create(
            title='Тестовая группа',
            slug=cls.GROUP_SLUG,
            description='Тестовое описание',
        )

        cls.post = Post.objects.create(
            author=cls.user,
            text=cls.POST_TEXT,
            group=cls.group,
        )
        cls.image = cls.post.image
        cls.post_id = cls.post.id
        cache.clear()

    def setUp(self):
        self.authorized_client = Client()
        self.authorized_client.force_login(self.user)

    def test_index_correct_cache(self):
        response = self.authorized_client.get(reverse('posts:index'))
        post_obj = response.context.get('page_obj')[0]
        self.assertEqual(post_obj.pk, self.post_id)
        cache_check = response.content
        Post.objects.get(pk=self.post_id).delete()
        response = self.authorized_client.get(reverse('posts:index'))
        self.assertEqual(response.content, cache_check)
        cache.clear()
        response = self.authorized_client.get(reverse('posts:index'))
        self.assertNotEqual(response.content, cache_check)

class PaginatorViewsTest(TestCase):
    @classmethod
    def setUpClass(cls):
        super().setUpClass()

        cls.POST_COUNT = 13
        cls.POSTS_ON_PAGE = 10
        cls.GROUP_SLUG = 'test_group_slug'

        cls.user = User.objects.create_user(username='test_user')

        cls.group = Group.objects.create(
            title='Тестовая группа',
            slug=cls.GROUP_SLUG,
            description='Тестовое описание',
        )

        for i in range(cls.POST_COUNT):
            cls.post = Post.objects.create(
                author=cls.user,
                text=f'Тестовый пост {i}',
                group=cls.group
            )
        cache.clear()

    def test_index_paginator_first_page(self):
        response = self.client.get(reverse('posts:index'))
        self.assertEqual(len(response.context['page_obj']), self.POSTS_ON_PAGE)

    def test_index_paginator_second_page(self):
        response = self.client.get(reverse('posts:index') + '?page=2')
        self.assertEqual(
            len(response.context['page_obj']),
            self.POST_COUNT - self.POSTS_ON_PAGE
        )

    def test_group_paginator_first_page(self):
        response = self.client.get(
            reverse('posts:group_list', kwargs={'slug': self.GROUP_SLUG})
        )
        self.assertEqual(len(response.context['page_obj']), self.POSTS_ON_PAGE)

    def test_group_paginator_second_page(self):
        response = self.client.get(
            reverse('posts:group_list', kwargs={'slug': self.GROUP_SLUG})
            + '?page=2'
        )
        self.assertEqual(
            len(response.context['page_obj']),
            self.POST_COUNT - self.POSTS_ON_PAGE
        )

    def test_profile_paginator_first_page(self):
        response = self.client.get(
            reverse('posts:profile', kwargs={'username': self.user.username})
        )
        self.assertEqual(len(response.context['page_obj']), self.POSTS_ON_PAGE)

    def test_profile_paginator_second_page(self):
        response = self.client.get(
            reverse('posts:profile', kwargs={'username': self.user.username})
            + '?page=2'
        )
        self.assertEqual(
            len(response.context['page_obj']),
            self.POST_COUNT - self.POSTS_ON_PAGE
        )