#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
DFUPD Client - МИНИМАЛЬНАЯ ВЕРСИЯ
Передача по 256 байт, минимальное использование памяти
"""

import socket
import struct
import time
import argparse
import os
import sys

# Константы
DFUPD_REQ_SIGNATURE = 0x44505546
DFUPD_RSP_SIGNATURE = 0x44555044
DFUPD_PROTOCOL_VERSION = 1

DFUPD_PAGE_SIZE = 256
DFUPD_SECTOR_SIZE = 4096

# Команды
CMD_ERASE_SECTORS = 0x01
CMD_WRITE_PAGE = 0x02
CMD_READ_PAGE = 0x03
CMD_GET_FLASH_INFO = 0x06
CMD_RESET = 0x10

# Статусы
STATUS_SUCCESS = 0x00
STATUS_ERR_INVALID = 0x01
STATUS_ERR_ADDR = 0x02
STATUS_ERR_SIZE = 0x03

STATUS_MESSAGES = {
    STATUS_SUCCESS: "OK",
    STATUS_ERR_INVALID: "Invalid command",
    STATUS_ERR_ADDR: "Invalid address",
    STATUS_ERR_SIZE: "Invalid size"
}

class DFUPDClient:
    def __init__(self, host, port=5008):
        self.host = host
        self.port = port
        self.sock = None
        
    def connect(self):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.settimeout(30.0)  # Увеличенный таймаут для стирания
        self.sock.connect((self.host, self.port))
        
    def close(self):
        if self.sock:
            self.sock.close()
            self.sock = None
            
    def _send_request(self, command, data=b''):
        """Отправка запроса (максимум 260 байт)"""
        # Заголовок 12 байт
        header = struct.pack('<IBBH I', 
                            DFUPD_REQ_SIGNATURE,
                            DFUPD_PROTOCOL_VERSION,
                            command,
                            0,
                            len(data))
        
        self.sock.send(header + data)
        
        # Получаем заголовок ответа
        resp_header = self.sock.recv(struct.calcsize('<IBBH I'))
        if len(resp_header) < struct.calcsize('<IBBH I'):
            raise Exception("Invalid response header")
        
        sig, ver, status, flags, length = struct.unpack('<IBBH I', resp_header)
        
        if sig != DFUPD_RSP_SIGNATURE:
            raise Exception("Invalid signature")
        
        # Получаем данные
        data = b''
        if length > 0:
            while len(data) < length:
                chunk = self.sock.recv(length - len(data))
                if not chunk:
                    raise Exception("Connection closed")
                data += chunk
        
        if status != STATUS_SUCCESS:
            raise Exception(f"Error: {STATUS_MESSAGES.get(status, f'code {status}')}")
        
        return data
    
    def get_flash_info(self):
        """Информация о flash"""
        data = self._send_request(CMD_GET_FLASH_INFO)
        fmt = '<HIIIIIII'
        info = struct.unpack(fmt, data[:struct.calcsize(fmt)])
        return {
            'flash_id': info[0],
            'total_size': info[1],
            'sector_size': info[2],
            'page_size': info[3],
            'num_sectors': info[4],
            'write_counter': info[5],
            'read_counter': info[6],
            'erase_counter': info[7]
        }
    
    def erase_sectors(self, start_addr, end_addr):
        """Стирание секторов"""
        print(f"Erasing 0x{start_addr:08X}-0x{end_addr:08X}...")
        data = struct.pack('<II', start_addr, end_addr)
        self._send_request(CMD_ERASE_SECTORS, data)
        print("OK")
    
    def write_page(self, addr, data):
        """Запись одной страницы (256 байт)"""
        if len(data) != DFUPD_PAGE_SIZE:
            raise Exception(f"Data must be {DFUPD_PAGE_SIZE} bytes")
        if addr % DFUPD_PAGE_SIZE != 0:
            raise Exception(f"Address 0x{addr:X} not aligned")
        
        req = struct.pack('<I', addr) + data
        self._send_request(CMD_WRITE_PAGE, req)
    
    def read_page(self, addr):
        """Чтение одной страницы"""
        if addr % DFUPD_PAGE_SIZE != 0:
            raise Exception(f"Address 0x{addr:X} not aligned")
        
        data = struct.pack('<I', addr)
        resp = self._send_request(CMD_READ_PAGE, data)
        
        read_addr, page_data = struct.unpack(f'<I{DFUPD_PAGE_SIZE}s', resp)
        return page_data
    
    def write_file(self, start_addr, file_path):
        """
        Запись файла по 256 байт за раз
        МИНИМАЛЬНОЕ ИСПОЛЬЗОВАНИЕ ПАМЯТИ
        """
        # Получаем размер файла
        file_size = os.path.getsize(file_path)
        num_pages = (file_size + DFUPD_PAGE_SIZE - 1) // DFUPD_PAGE_SIZE
        
        print(f"Writing {file_path} to 0x{start_addr:08X}")
        print(f"Size: {file_size} bytes, Pages: {num_pages}")
        
        # Вычисляем секторы для стирания
        end_addr = start_addr + (num_pages * DFUPD_PAGE_SIZE) - 1
        start_sector = start_addr // DFUPD_SECTOR_SIZE
        end_sector = end_addr // DFUPD_SECTOR_SIZE
        
        print(f"Erasing sectors {start_sector}-{end_sector}...")
        self.erase_sectors(start_addr, end_addr)
        
        # Запись по страницам
        print("Writing pages...")
        with open(file_path, 'rb') as f:
            for page in range(num_pages):
                addr = start_addr + page * DFUPD_PAGE_SIZE
                
                # Читаем страницу из файла
                page_data = f.read(DFUPD_PAGE_SIZE)
                
                # Дополняем последнюю страницу FF
                if len(page_data) < DFUPD_PAGE_SIZE:
                    page_data = page_data.ljust(DFUPD_PAGE_SIZE, b'\xFF')
                
                # Записываем
                self.write_page(addr, page_data)
                
                # Прогресс
                if (page + 1) % 10 == 0:
                    print(f"  {page + 1}/{num_pages} pages")
        
        print(f"Done! {num_pages} pages written")
    
    def read_file(self, start_addr, file_size, output_path):
        """Чтение файла по 256 байт"""
        num_pages = (file_size + DFUPD_PAGE_SIZE - 1) // DFUPD_PAGE_SIZE
        
        print(f"Reading {file_size} bytes from 0x{start_addr:08X}")
        print(f"Pages: {num_pages}")
        
        with open(output_path, 'wb') as f:
            for page in range(num_pages):
                addr = start_addr + page * DFUPD_PAGE_SIZE
                page_data = self.read_page(addr)
                
                # Для последней страницы обрезаем
                if page == num_pages - 1:
                    last_size = file_size - page * DFUPD_PAGE_SIZE
                    page_data = page_data[:last_size]
                
                f.write(page_data)
                
                if (page + 1) % 10 == 0:
                    print(f"  {page + 1}/{num_pages} pages")
        
        print(f"Saved to {output_path}")
    
    def reset(self):
        """Перезагрузка"""
        try:
            self._send_request(CMD_RESET)
        except:
            pass
        self.close()

def format_size(size):
    for unit in ['B', 'KB', 'MB']:
        if size < 1024:
            return f"{size:.1f} {unit}"
        size /= 1024
    return f"{size:.1f} GB"

def main():
    parser = argparse.ArgumentParser(description='DFUPD Client (MINIMAL)')
    parser.add_argument('host', help='Device IP')
    parser.add_argument('cmd', choices=['info', 'erase', 'write', 'read', 'reset'])
    parser.add_argument('--addr', type=lambda x: int(x, 0), help='Address')
    parser.add_argument('--end', type=lambda x: int(x, 0), help='End address')
    parser.add_argument('--file', help='File path')
    parser.add_argument('--size', type=int, help='Size to read')
    parser.add_argument('--port', type=int, default=5008)
    
    args = parser.parse_args()
    
    client = DFUPDClient(args.host, args.port)
    
    try:
        client.connect()
        
        if args.cmd == 'info':
            info = client.get_flash_info()
            print("\n" + "="*40)
            print("FLASH INFO")
            print("="*40)
            print(f"ID:         0x{info['flash_id']:04X}")
            print(f"Size:       {info['total_size']} ({format_size(info['total_size'])})")
            print(f"Sector:     {info['sector_size']} bytes")
            print(f"Page:       {info['page_size']} bytes")
            print(f"Sectors:    {info['num_sectors']}")
            print(f"Write cnt:  {info['write_counter']}")
            print(f"Read cnt:   {info['read_counter']}")
            print(f"Erase cnt:  {info['erase_counter']}")
            print("="*40)
            
        elif args.cmd == 'erase':
            if args.addr is None or args.end is None:
                print("Error: --addr and --end required")
                return 1
            client.erase_sectors(args.addr, args.end)
            
        elif args.cmd == 'write':
            if args.addr is None or args.file is None:
                print("Error: --addr and --file required")
                return 1
            client.write_file(args.addr, args.file)
            
        elif args.cmd == 'read':
            if args.addr is None or args.size is None or args.file is None:
                print("Error: --addr, --size and --file required")
                return 1
            client.read_file(args.addr, args.size, args.file)
            
        elif args.cmd == 'reset':
            client.reset()
            
    except Exception as e:
        print(f"Error: {e}")
        return 1
    finally:
        client.close()
    
    return 0

if __name__ == '__main__':
    sys.exit(main())