/*
 * Copyright (c) 2003 Apple Computer, Inc. All rights reserved.
 *
 * @APPLE_LICENSE_HEADER_START@
 *
 * The contents of this file constitute Original Code as defined in and
 * are subject to the Apple Public Source License Version 1.1 (the
 * "License").  You may not use this file except in compliance with the
 * License.  Please obtain a copy of the License at
 * http://www.apple.com/publicsource and read it before using this file.
 *
 * This Original Code and all software distributed under the License are
 * distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, EITHER
 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT.  Please see the
 * License for the specific language governing rights and limitations
 * under the License.
 *
 * @APPLE_LICENSE_HEADER_END@
 */

#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <sys/sockio.h>
#include <sys/time.h>
#include <ctype.h>
#include <net/if.h>
#include <netinet/in.h>
#include <netinet6/in6_var.h>
#include <syslog.h>

#include "globals.h"
#include "timer.h"
#include "configthreads_common.h"
#include "ip6config_utils.h"

typedef struct {
    timer_callout_t *	timer;
    struct in6_addr	our_ip;
    int			our_prefixLen;
} Service_manual_t;

static void
manual_cancel_pending_events(Service_t * service_p)
{
    Service_manual_t *	manual = (Service_manual_t *)service_p->private;

    if (manual == NULL)
        return;
    if (manual->timer) {
        timer_cancel(manual->timer);
    }
    return;
}

static void
manual_inactive(Service_t * service_p)
{
    service_remove_address(service_p);
    service_publish_failure(service_p, ip6config_status_media_inactive_e,
			    NULL);
    return;
}

static void
manual_link_timer(void * arg0, void * arg1, void * arg2)
{
    manual_inactive((Service_t *) arg0);
    return;
}

static ip6config_status_t
validate_method_data_addresses(config_data_t * cfg, ip6config_method_t method,
                                char * ifname)
{
    int		i;
    char	str[INET6_ADDRSTRLEN];
    
    if (cfg->data->n_ip6 < 1) {
        my_log(LOG_DEBUG, "%s %s: no IP6 addresses specified",
                ip6config_method_string(method), ifname);
        return (ip6config_status_invalid_parameter_e);
    }
    for (i = 0; i < cfg->data->n_ip6; i++) {
        if (ip_valid(&cfg->data->ip6[i].addr) == FALSE) {
            my_log(LOG_DEBUG, "%s %s: invalid IP6 %s",
                ip6config_method_string(method), ifname,
                inet_ntop(AF_INET6, &cfg->data->ip6[i].addr, str, sizeof(str)));
            return (ip6config_status_invalid_parameter_e);
        }
    }
    return (ip6config_status_success_e);
}

static void
manual_start(Service_t * service_p)
{
    Service_manual_t *	manual = (Service_manual_t *)service_p->private;

    manual_cancel_pending_events(service_p);

    if (service_link_status(service_p)->valid == TRUE
            && service_link_status(service_p)->active == FALSE) {
            manual_inactive(service_p);
        return;
    }

    /* set the new address */
    (void)service_set_address(service_p, &manual->our_ip,
                                manual->our_prefixLen);

    return;
}

__private_extern__ ip6config_status_t
manual_thread(Service_t * service_p, IFEventID_t evid, void * event_data)
{
    interface_t *	if_p = service_interface(service_p);
    Service_manual_t *	manual = (Service_manual_t *)service_p->private;
    ip6config_status_t	status = ip6config_status_success_e;

    switch (evid) {
        case IFEventID_start_e: {
            start_event_data_t *    evdata = ((start_event_data_t *)event_data);
            ip6config_method_data_t *ipcfg = evdata->config.data;
    
            my_log(LOG_DEBUG, "MANUAL_THREAD %s: STARTING", if_name(if_p));
    
            if (manual) {
                my_log(LOG_DEBUG, "MANUAL_THREAD %s: re-entering start state",
                        if_name(if_p));
                status = ip6config_status_internal_error_e;
                break;
            }
        
            status = validate_method_data_addresses(&evdata->config,
                            ip6config_method_manual_e, if_name(if_p));
            if (status != ip6config_status_success_e) {
                my_log(LOG_DEBUG, "MANUAL_THREAD: validate_method_data_addresses returned error");
                break;
            }
    
            manual = calloc(1, sizeof(*manual));
            if (manual == NULL) {
                my_log(LOG_ERR, "MANUAL_THREAD %s: calloc failed",
                        if_name(if_p));
                status = ip6config_status_allocation_failed_e;
                break;
            }
        
            service_p->private = manual;
            memcpy(&manual->our_ip, &(ipcfg->ip6[0].addr), sizeof(struct in6_addr));
            manual->our_prefixLen = ipcfg->ip6[0].prefixLen;
            if (if_flags(if_p) & IFF_LOOPBACK) {
                /* set the new address */
                (void)service_set_address(service_p, &manual->our_ip,
                        manual->our_prefixLen);
                service_publish_success(service_p);
                break;
            }
    
            manual->timer = timer_callout_init();
            if (manual->timer == NULL) {
                my_log(LOG_ERR, "MANUAL_THREAD %s: timer_callout_init failed",
                        if_name(if_p));
                status = ip6config_status_allocation_failed_e;
                goto stop;
            }
        
            my_log(LOG_DEBUG, "MANUAL_THREAD %s: starting", if_name(if_p));
            manual_start(service_p);
            break;
        }
        stop:
        case IFEventID_stop_e: {
            my_log(LOG_DEBUG, "MANUAL_THREAD %s: STOPPING", if_name(if_p));

            if (manual == NULL) {
                my_log(LOG_DEBUG, "MANUAL %s: private data is NULL",
                        if_name(if_p));
                status = ip6config_status_internal_error_e;
                break;
            }

            /* remove IP6 address */
            service_remove_address(service_p);

            /* clean-up resources */
            if (manual->timer) {
                timer_callout_free(&manual->timer);
            }

            free(manual);
            service_p->private = NULL;
            break;
        }
        case IFEventID_change_e: {
            change_event_data_t *	evdata = ((change_event_data_t *)event_data);
            ip6config_method_data_t *	ipcfg = evdata->config.data;

            my_log(LOG_DEBUG, "MANUAL_THREAD %s: CHANGING", if_name(if_p));

            if (manual == NULL) {
                my_log(LOG_DEBUG, "MANUAL %s: private data is NULL",
                        if_name(if_p));
                status = ip6config_status_internal_error_e;
                break;
            }

            status = validate_method_data_addresses(&evdata->config,
					    ip6config_method_manual_e, if_name(if_p));
            if (status != ip6config_status_success_e)
                break;

            evdata->needs_stop = FALSE;

            if (!IN6_ARE_ADDR_EQUAL(&ipcfg->ip6[0].addr, &manual->our_ip)
                    || (ipcfg->ip6[0].prefixLen - manual->our_prefixLen != 0)) {
                evdata->needs_stop = TRUE;
            }
            break;
        }
        case IFEventID_state_change_e: {
            int	i;
            ip6_addrinfo_list_t * 	ip6_addrs = ((ip6_addrinfo_list_t *)event_data);
            inet6_addrinfo_t *		info_p = &service_p->info;

            my_log(LOG_DEBUG, "MANUAL_THREAD %s: STATE CHANGE", if_name(if_p));

            if (manual == NULL) {
                my_log(LOG_DEBUG, "MANUAL %s: private data is NULL",
                        if_name(if_p));
                status = ip6config_status_internal_error_e;
                break;
            }
                
            /* go through the address list; if addr is not autoconf and
             * not linklocal then deal with it
             */
            for (i = 0; i < ip6_addrs->n_addrs; i++) {
                ip6_addrinfo_t	new_addr = ip6_addrs->addr_list[i];

                if (!IN6_IS_ADDR_LINKLOCAL(&new_addr.addr) 
                        && !(new_addr.flags & IN6_IFF_AUTOCONF)
                        && IN6_ARE_ADDR_EQUAL(&new_addr.addr, &info_p->addr)) {
                    /* check for duplicate */
                    if (new_addr.flags & IN6_IFF_DUPLICATED) {
                        char	msg[128];

                        snprintf(msg, sizeof(msg),
                                    IP6_FORMAT " is a duplicate address on interface %s",
                                    IP6_LIST(&manual->our_ip), if_name(if_p));
                        service_report_conflict(service_p, &manual->our_ip);
                        my_log(LOG_ERR, "MANUAL %s: %s",
                                if_name(if_p), msg);
                        service_remove_address(service_p);
                        service_publish_failure(service_p,
                                ip6config_status_address_in_use_e,
                                msg);
                        break;
                    }

                    service_publish_success(service_p);
                    break;
                }
            }
                
            break;
        }
        case IFEventID_media_e: {
            if (manual == NULL)
                return (ip6config_status_internal_error_e);

            if (service_link_status(service_p)->valid == TRUE) {
                if (service_link_status(service_p)->active == TRUE) {
                    manual_start(service_p);
                }
                else {
                    struct timeval tv;

                    /* if link goes down and stays down long enough, unpublish */
                    manual_cancel_pending_events(service_p);
                    tv.tv_sec = LINK_INACTIVE_WAIT_SECS;
                    tv.tv_usec = 0;
                    timer_set_relative(manual->timer, tv,
                                    (timer_func_t *)manual_link_timer,
                                    service_p, NULL, NULL);
                }
            }
            break;
        }
        default:
            break;
    } /* switch */

    return (status);
}
