class LibraryReducers():
    def go_to_book(self, state, number):
        width, height = state_helpers.dimensions(state)
        page = state['library']['page']
        line_number = page * (height - 1)
        books = state_helpers.get_books(state)
        try:
            book = books[line_number + number]
        except Exception:
            log.debug('no book at {}'.format(number))
            return state
        user = state['user'].copy(current_book=book.filename)
        return state.copy(location='book', user=user, home_menu_visible=False)

    def add_or_replace(self, state, book):
        books = OrderedDict(state['user']['books'])
        books[book.filename] = book
        books = sort_books(books)
        return state.copy(user=state['user'].copy(books=books))

    def set_book_loading(self, state, book):
        book = book._replace(load_state=book_file.LoadState.LOADING)
        return self.add_or_replace(state, book)

    def remove_book(self, state, book):
        books = OrderedDict(state['user']['books'])
        if book.filename in books:
            del books[book.filename]
        books = FrozenOrderedDict(books)
        return state.copy(user=state['user'].copy(books=books))

    def replace_library(self, state, value):
        if state['replacing_library'] == 'in progress' and value is False:
            return state
        else:
            return state.copy(replacing_library=value, location='library') class BookReducers():
    def go_to_start(self, state, value):
        return self.set_book_page(state, 0)

    def go_to_end(self, state, value):
        width, height = state_helpers.dimensions(state)
        book = state_helpers.get_current_book(state)
        last_page = book.get_num_pages() - 1
        return self.set_book_page(state, last_page)

    def set_book_page(self, state, page):
        width, height = state_helpers.dimensions(state)
        book_n = state['user']['current_book']
        books = OrderedDict(state['user']['books'])
        book = books[book_n].set_page(page)
        bookmarks = tuple(bm for bm in book.bookmarks if bm != 'deleted')
        book = book._replace(bookmarks=bookmarks)
        books[book_n] = book
        return state.copy(user=state['user'].copy(books=FrozenOrderedDict(books)),
                          location='book', home_menu_visible=False)

    def enter_go_to_page(self, state, value):
        return state.copy(home_menu_visible=False, location='go_to_page')

    def toggle_home_menu(self, state, value):
        return state.copy(home_menu_visible=not state['home_menu_visible'])

    def insert_bookmark(self, state, _):
        number = state['user']['current_book']
        books = OrderedDict(state['user']['books'])
        book = books[number]
        page = book.page_number
        if page not in book.bookmarks:
            bookmarks = sorted(book.bookmarks + tuple([page]))
            book = book._replace(bookmarks=tuple(bookmarks))
            books[number] = book
            return state.copy(user=state['user'].copy(books=FrozenOrderedDict(books)))
        return state class GoToPageReducers():
    def go_to_page_set_selection(self, state, _):
        go_to_page = state['go_to_page']
        selection = go_to_page['selection'] + go_to_page['keys_pressed']

        # handle delete ('<') characters
        r = re.compile(r'\d<')
        if r.search(selection) is not None:
            selection = ''
        # any left over delete characters after numbers are ignored
        selection = ''.join([c for c in selection if c != '<'])

        # overwrite characters when exceeding max page num width
        num_width = state_helpers.get_page_num_width(state)
        selection = selection[-num_width:]

        go_to_page = go_to_page.copy(selection=selection, keys_pressed='')
        return state.copy(go_to_page=go_to_page)

    def go_to_page_key_press(self, state, value):
        go_to_page = state['go_to_page']
        keys_pressed = go_to_page['keys_pressed'] + str(value)
        go_to_page = go_to_page.copy(keys_pressed=keys_pressed)
        return state.copy(go_to_page=go_to_page)

    def go_to_page_confirm(self, state, value):
        state = self.go_to_page_set_selection(state, None)
        go_to_page = state['go_to_page']
        selection = go_to_page['selection']
        if selection == '':
            return state
        go_to_page = go_to_page.copy(selection='')
        page = int(selection) - 1
        set_book_page = BookReducers().set_book_page
        return set_book_page(state.copy(go_to_page=go_to_page), page)

    def go_to_page_delete(self, state, value):
        go_to_page = state['go_to_page']
        selection = go_to_page['selection'][0:-1]
        return state.copy(go_to_page=go_to_page.copy(selection=selection)) class BookmarksReducers():
    def delete_bookmark(self, state, n):
        width, height = state_helpers.dimensions(state)
        # adjust for title
        height -= 1
        page = state['bookmarks_menu']['page']
        book_n = state['user']['current_book']
        books = OrderedDict(state['user']['books'])
        book = books[book_n]
        bookmarks = book.bookmarks
        line = page * height
        # don't delete go-to-start end and go-to-end bookmarks
        if (line + n) == 0 or (line + n) == (len(bookmarks) - 1):
            return state
        changed_bookmarks = list(bookmarks[line:line + height])
        if n >= len(changed_bookmarks):
            return state
        changed_bookmarks[n] = 'deleted'
        bookmarks = bookmarks[0:line] + tuple(changed_bookmarks) \
            + bookmarks[line + height:len(bookmarks)]
        books[book_n] = book._replace(bookmarks=bookmarks)
        return state.copy(user=state['user'].copy(books=FrozenOrderedDict(books)))

    def go_to_bookmark(self, state, n):
        width, height = state_helpers.dimensions(state)
        # adjust for title
        height -= 1
        page = state['bookmarks_menu']['page']
        book = state_helpers.get_current_book(state)
        bookmarks = book.bookmarks[page * height:(page * height) + height]
        if n >= len(bookmarks):
            return state
        bookmark = bookmarks[n]
        if bookmark == 'deleted':
            return state
        set_book_page = BookReducers().set_book_page
        return set_book_page(state, bookmark) class LanguageReducers():
    def select_language(self, state, value):
        languages = state['languages']
        lang = abs(int(value))
        keys = list(languages['available'].keys())
        if lang < len(keys):
            locale = keys[lang]
            install(locale)
            manual = Manual.create()
            books = state['user']['books'].copy(**{manual_filename: manual})
            user = state['user'].copy(current_language=locale, books=books)
            return state.copy(location='book', user=user)
        else:
            return state def render_book_help(width, height):
    # TRANSLATORS: The AsciiDoc markup is used to suggest a page break
    # in this string.
    text = _('''\
With Canute 360 you can read files in Braille. You can move within a \
file using the three large control buttons on the front panel of \
Canute. Press the large button to the right of centre on the front \
panel labelled "forward" to move forward one page within this help \
file.

<<<

Press the large button to the left of centre, labelled "back" to move \
back one page within a file, similar to turning the pages in a \
physical book. You can move forwards of backwards five pages at a time \
by holding down the "forward" or "back" button. You can access all \
other Canute features, including the library menu, bookmarks and \
system settings by pressing the large central button labelled "menu" \
to access the main menu. Press the circular help button (labelled "H") \
at the top left of the display to return to your book.\
''')

    return _render(width, height, text) def render_home_menu_help(width, height):
    text = _('''\
This is the main menu. From the main menu you can access the library, \
insert a bookmark, return to a bookmark that was previously placed \
within a file, or navigate to a page within a book. You can select an \
item from the main menu by pressing the triangular line select button \
to the left of the menu item on the display.

You can choose a new book by pressing the line select button to the \
left of "view library menu" on the display to access the library menu. \
You can navigate to a page within a file by pressing the triangular \
line select button to the left of "go to page" and following the \
instructions on the display. You can insert a bookmark by pressing the \
line select button to the left of "insert bookmark at current page". \
You can retrieve a bookmark by pressing the line select button to the \
left of "choose from existing bookmarks". To make system wide changes, \
including changing the language or Braille code, press the line select \
button to the left of "view system menu" on the display.\
''')

    return _render(width, height, text) def render_help(width, height):
    data = []
    para = _('''\
This is the library menu. From this menu you can view and select from \
the files on the memory stick or SD card. You can choose a file by \
pressing the line select button to the left of the file name. Canute \
360 will then show your chosen file on the display. You can navigate \
forward and backwards within the library menu using the large buttons \
labelled "forward" and "back" on the front panel. You can return to \
the file you are currently reading by pressing the large central \
button labelled "menu".

To load a new file onto your Canute, first turn it off by pressing and \
then quickly releasing the small button to the right of the power \
socket on the back panel. Once the "please wait" text disappears, \
remove the memory stick or SD card you are using to store your files. \
Copy and paste your files, in BRF or PEF format, onto the memory stick \
or SD card using a computer. Finally, insert the USB stick or SD card \
into the slot on the side of Canute. Turn your Canute on again using \
the small button to the right of the power socket on the back panel. \
Once Canute has started your files will appear in the library menu.

For best results you should format your files with forty cells per \
line and nine lines of Braille per page. The latest version of Duxbury \
DBT and the free online robo-braille service both have a Canute 360 \
preset built in for formatting.\
''')

    for line in para.split('\n'):
        data.append(from_unicode(line))

    while len(data) % height:
        data.append(tuple())

    return tuple(data) def render_help(width, height):
    data = []
    para = _('''\
This is the system menu. From the system menu you can make system wide \
changes to settings.

You can change the language or Braille code by pressing the line \
select button to the left of select language. Please note this will \
only change the system language or Braille code, not the language or \
code of any files in the library.\
''')

    for line in para.split('\n'):
        data.append(from_unicode(line))

    while len(data) % height:
        data.append(tuple())

    return tuple(data) def render_help(width, height):
    data = []
    para = _('''\
You can change the system language or Braille code by pressing the \
line select button to the left of your chosen language or code. The \
language or code in the contextual help, menus and system text will \
then change to your selected language.\
''')

    for line in para.split('\n'):
        data.append(from_unicode(line))

    while len(data) % height:
        data.append(tuple())

    return tuple(data) def render_help(width, height):
    data = []
    para = _('''\
You can navigate to a page within a file by entering the page number \
using the line select buttons on the left hand side the display. For \
example, for page 10 press button 1 followed by button 0. Press the \
forward button to confirm your selection. Line 3 will refresh to show \
your selected page. Press the large forward button on the front \
surface to navigate to your page.

If you have entered the wrong page number you can reset your selection \
by pressing the large back button on the front surface.\
''')

    for line in para.split('\n'):
        data.append(from_unicode(line))

    while len(data) % height:
        data.append(tuple())

    return tuple(data) def render_help(width, height):
    data = []

    para = _('''\
To navigate to a bookmark within a file press the line select button \
immediately to the left of your chosen bookmark. From the bookmark \
menu you can also navigate to the beginning of your book by selecting \
"start of book", or the end of a book by selecting "end of book", \
using the line select buttons. You can delete a bookmark by pressing \
and holding the line select button to the left of the bookmark you \
wish to delete for three seconds. You cannot delete the "start of \
book" or "end of book" bookmarks.\
''')

    for line in para.split('\n'):
        data.append(from_unicode(line))

    # pad page with empty rows
    while len(data) % height:
        data.append(tuple())

    return tuple(data)
import logging
from frozendict import frozendict, FrozenOrderedDict
from collections import OrderedDict
from . import utility
from . import state_helpers
from .library.reducers import LibraryReducers
from .book.reducers import BookReducers
from .go_to_page.reducers import GoToPageReducers
from .bookmarks.reducers import BookmarksReducers
from .language.reducers import LanguageReducers
from .book.help import render_book_help, render_home_menu_help
from .library.view import render_help as render_library_help
from .system_menu.help import render_help as render_system_help
from .language.view import render_help as render_language_help
from .go_to_page.view import render_help as render_gtp_help
from .bookmarks.help import render_help as render_bookmarks_help



log = logging.getLogger(__name__)


class AppReducers():
    def set_user_state(self, state, value):
        return state.copy(user=value)

    def trigger(self, state, value):
        '''bit ugly but gives the ability to trigger any state subscribers'''
        return state.copy()

    def set_dimensions(self, state, value):
        dimensions = frozendict(width=value[0], height=value[1])
        return state.copy(dimensions=dimensions)

    def go_to_library(self, state, _):
        return state.copy(location='library')

    def go_to_system_menu(self, state, _):
        return state.copy(location='system_menu')

    def go_to_bookmarks_menu(self, state, _):
        return state.copy(location='bookmarks_menu')

    def go_to_language_menu(self, state, _):
        return state.copy(location='language')

    def toggle_help_menu(self, state, _):
        visible = state['help_menu']['visible']
        help_menu = frozendict(visible=not visible, page=0)
        return state.copy(help_menu=help_menu)

    def close_menu(self, state, value):
        books = state['user']['books']
        # fully delete deleted bookmarks
        changed_books = OrderedDict()
        for filename in books:
            book = books[filename]
            bookmarks = tuple(bm for bm in book.bookmarks if bm != 'deleted')
            book = book._replace(bookmarks=bookmarks)
            changed_books[filename] = book
        bookmarks_menu = state['bookmarks_menu']
        return state.copy(location='book',
                          bookmarks_menu=bookmarks_menu.copy(page=0),
                          home_menu_visible=False, go_to_page_selection='',
                          help_menu=frozendict({'visible': False, 'page': 0}),
                          user=state['user'].copy(books=FrozenOrderedDict(changed_books)))

    def go_to_page(self, state, page):
        width, height = state_helpers.dimensions(state)

        if page < 0:
            page = 0

        if state['help_menu']['visible']:
            location = 'help_menu'
        else:
            location = state['location']

        if location == 'library':
            books = state['user']['books']
            max_pages = (len(books) - 1) // (height - 1)
            if page > max_pages:
                page = max_pages
            elif page < 0:
                page = 0
            library = frozendict({'page': page})
            return state.copy(library=library)
        elif location == 'book':
            book_n = state['user']['current_book']
            book = state['user']['books'][book_n]
            books = OrderedDict(state['user']['books'])
            book = book.set_page(page)
            books[book_n] = book
            return state.copy(user=state['user'].copy(books=FrozenOrderedDict(books)))
        elif location == 'bookmarks_menu':
            book_n = state['user']['current_book']
            book = state['user']['books'][book_n]
            bookmarks_data = book.bookmarks
            # Account for title line.
            effective_height = height - 1
            num_pages = (len(bookmarks_data) + (effective_height-1)) // effective_height
            if page >= num_pages - 1:
                page = num_pages - 1
            bookmarks_menu = state['bookmarks_menu'].copy(page=page)
            return state.copy(bookmarks_menu=bookmarks_menu)
        elif location == 'language':
            lang_n = state['user'].get('current_language', 'en_GB:en')
            lang = list(state['languages']['available'].keys())[lang_n]
            language_menu = state['user'].copy(current_language=lang)
            return state.copy(language=language_menu)
        elif location == 'help_menu':

            # To calculate help page bounds and ensure we stay within
            # them, do a dummy render of the help and count the pages.

            mapping = {
                'book': render_book_help,
                'library': render_library_help,
                'system_menu': render_system_help,

                'language': render_language_help,