#!/bin/sh
# Copyright (c) 2016 Qualcomm Atheros, Inc.
#
# All Rights Reserved.
# Qualcomm Atheros Confidential and Proprietary.

. /usr/share/libubox/jshn.sh
. /lib/functions/repacd-netdet.sh

LP_DIRECTION_UPSTREAM="upstream"
LP_DIRECTION_DOWNSTREAM="downstream"
LP_DIRECTION_UNKNOWN="unknown"
LP_ROLE_CAP="CAP"
LP_ROLE_RE="RE"
LP_VLAN_OFFSET=10

LP_DIRECTION_DB="/etc/ports.dir"

EDMA_ETH0_PORTBMP="/proc/sys/net/edma/default_group1_bmp"
EDMA_ETH1_PORTBMP="/proc/sys/net/edma/default_group2_bmp"

#TODO - these values need to be determined by board
#Port number of the port in the eth0 vlan by default
LP_ETH0_PORT=5
#Port number of the cpu port including the "t" for tagging if appropriate
LP_CPU_PORT="0t"

#TODO - these should probably be configurable from outside ths script
#Timeout for dhcp requests from CA
LP_DHCP_TO_CAP=15
#Timeout for dhcp requests from RE (needs to be longer than from CA)
LP_DHCP_TO_RE=30

__lp_loginfo() {
    logger -t repacd.lp -p user.info "$1"
}

__lp_logdebug() {
    logger -t repacd.lp -p user.debug "$1"
}

#Returns the default interface for the given port
lp_defaultinterface() {
    if [ $port = $LP_ETH0_PORT ]; then
        echo "eth0"
    else
        echo "eth1"
    fi
}
#Returns the isolated inteface name for a given port
lp_interfacebyport() {
    local port=$1
    local baseinterface=$(lp_defaultinterface $port)
    local interface="${baseinterface}.$(( $port + $LP_VLAN_OFFSET ))"
    echo $interface
}

#Returns the role of the device, either "CAP" or "RE"
lp_devicerole() {
    #A CAP should have an entry for network.wan.ifname, an RE should not
    uci get network.wan.ifname >/dev/null
    if [ $? = 0 ]; then
        echo $LP_ROLE_CAP
    else
        echo $LP_ROLE_RE
    fi
}

#Returns the determined direction (upstream, downstram, or unknown) for the
#given port
lp_getportdirection() {
    local port=$1
    local direction=$LP_DIRECTION_UNKNOWN
    if [ -f $LP_DIRECTION_DB ]; then
        json_load "$(cat $LP_DIRECTION_DB)"
        json_select "port" >/dev/null
        json_get_var direction $port
    fi

    echo $direction
}

#set the current direction (upstream, downstream, or unknown) in a
#persistent db
lp_setportdb() {
    local port=$1
    local direction=$2
    local current
    local new
    local nports=$(lp_numports)
    local dir

    if [ -f $LP_DIRECTION_DB ]; then
        current=$(cat $LP_DIRECTION_DB)
    else
        current="{}"
    fi
    json_init
    json_add_array "port"
    new=$(json_dump)
    for p in $(seq 1 $nports); do
        if [ $p = $port ]; then
            dir=$direction
        else
            json_load "$current"
            dir=$LP_DIRECTION_UNKNOWN
            json_select "port" >/dev/null
            json_get_var dir $p
        fi
        json_load "$new"
        json_select "port"
        json_add_string $p $dir
        new=$(json_dump)
    done
    json_close_array
    new=$(json_dump)

    local tmpfile="$(dirname $LP_DIRECTION_DB)/.$(basename $LP_DIRECTION_DB)"
    echo "$new" > $tmpfile
    mv $tmpfile $LP_DIRECTION_DB
}

#Returns the current number of ports which are marked upstream
lp_numupstream() {
    local nup=0
    local dir
    local nports=$(lp_numports)

    if [ -f $LP_DIRECTION_DB ]; then
        json_load "$(cat $LP_DIRECTION_DB)"
        json_select "port" >/dev/null
        for p in $(seq 1 $nports); do
            dir=$LP_DIRECTION_UNKNOWN
            json_get_var dir $p
            if [ $dir = $LP_DIRECTION_UPSTREAM ]; then
                nup=$(( nup + 1 ))
            fi
        done
    fi
    echo $nup
}


#Returns the number of ethernet ports on switch0
lp_numports() {
    local lines=$(swconfig dev switch0 show | grep ports | wc -l)
    local words=$(swconfig dev switch0 show | grep ports | wc -w)

    echo $(( (( $words - $lines )) - $lines ))
}

#Determines whether the given interface is upstream or downstream
lp_determinedirection() {

    local port=$1
    #If netdet has already determined that this is a CAP device
    #then we will already have an upstream connection, and all
    #subsequent connections (including this one) should be
    #considered downstream
    local netdetmode=$(netdet_get_mode_db)
    local upstrmports=$(lp_numupstream)
    local portdirection=$(lp_getportdirection $port)

    logger -t repacd.lp -p user.debug "netdetmode: $netdetmode"
    logger -t repacd.lp -p user.debug "upstrmports: $upstrmports"
    if [ $netdetmode = "cap" ] && [ $upstrmports -gt 0 ] && [ $portdirection != $LP_DIRECTION_UPSTREAM ]; then
        logger -t repacd.lp -p user.info "Port $port assumed downstream on cap"
        echo $LP_DIRECTION_DOWNSTREAM
    else
        local role=$2
        local iface=$(lp_interfacebyport $port)
        local timeout
        if [ $role = $LP_ROLE_CAP ]; then
            timeout=$LP_DHCP_TO_CAP
        else
            timeout=$LP_DHCP_TO_RE
        fi

        if udhcpc -i $iface -t $timeout -T1 -n -q -R > /dev/null 2>&1; then
            __lp_loginfo "Port $port determined upstream"
            echo $LP_DIRECTION_UPSTREAM
        else
            __lp_loginfo "Port $port determined downstream"
            echo $LP_DIRECTION_DOWNSTREAM
        fi
    fi
}

#Isolate port in its own vlan and create virtual interface
lp_isolateport() {
    local port=$1
    local isovlan=$(( $port + $LP_VLAN_OFFSET ))
    local currvlan=$(swconfig dev switch0 port $port get pvid)
    local currports=$(swconfig dev switch0 vlan $currvlan show | grep ports | cut -d: -f2)
    local newports=""

    __lp_loginfo "Isolating switch port $port into its own vlan"
    #remove port from its current vlan
    for p in $currports; do
        if [ $p != $port ]; then
            newports="$newports $p"
        fi
    done
    __lp_logdebug "swconfig dev switch0 vlan $currvlan set ports \"$LP_CPU_PORT $newports\""
    swconfig dev switch0 vlan $currvlan set ports "$LP_CPU_PORT $newports"

    #add port to isovlan
    currports=$(swconfig dev switch0 vlan $isovlan show | grep ports | cut -d: -f2)
    __lp_logdebug "swconfig dev switch0 vlan $isovlan set ports \"$LP_CPU_PORT $currports $port\""
    swconfig dev switch0 vlan $isovlan set ports "$LP_CPU_PORT $currports $port"
    __lp_logdebug "swconfig dev switch0 set apply"
    swconfig dev switch0 set apply

    if [ $port = $LP_ETH0_PORT ]; then
        __lp_logdebug "vconfig add eth0 $isovlan"
        vconfig add eth0 $isovlan
    else
        __lp_logdebug "vconfig add eth1 $isovlan"
        vconfig add eth1 $isovlan
    fi
    lp_setedmamasks $port $(lp_defaultinterface $port)
}

lp_deisolateport() {
    local port=$1
    local isovlan=$(( $port + $LP_VLAN_OFFSET ))
    local currvlan=$(swconfig dev switch0 port $port get pvid)

    if [ $isovlan = $currvlan ]; then
        #This check might not be necessary, but it prevents us from
        #manipulating a port which is not isolated

        local isoiface=$(lp_interfacebyport $port)
        local unisovlan

        #Remove port form the isolated vlan
        swconfig dev switch0 vlan $isovlan set ports ""
        #Remove virtual interface
        vconfig rem $isoiface
        #put port back in the appropriate eth0 or eth1 vlan
        if [ $port = $LP_ETH0_PORT ]; then
            unisovlan=2
        else
            unisovlan=1
        fi

        local currports=$(swconfig dev switch0 vlan $unisovlan show | grep ports | cut -d: -f2)
        swconfig dev switch0 vlan $unisovlan set ports "$currports $port"
        swconfig dev switch0 set apply
    fi
}

#set the edma masks so the bitmap for the given iface (eth0 or eth1)
#contains the given port, and the bitmask fot the other iface does not
lp_setedmamasks() {
    local port=$1
    local dest_iface=$2
    local currmask_eth0=$(cat $EDMA_ETH0_PORTBMP)
    local currmask_eth1=$(cat $EDMA_ETH1_PORTBMP)
    local newmask_eth0
    local newmask_eth1

    if [ $dest_iface = "eth0" ]; then
        newmask_eth0=$(( $currmask_eth0 | (1 << $port) ))
        newmask_eth1=$(( $currmask_eth1 & ~(1 << $port) ))
    elif [ $dest_iface = "eth1" ]; then
        newmask_eth0=$(( $currmask_eth0 & ~(1 << $port) ))
        newmask_eth1=$(( $currmask_eth1 | (1 << $port) ))
    else
        #should not get here, but if we do do not adjust either bitmap
        newmask_eth0=$currmask_eth0
        newmask_eth1=$currmask_eth1
    fi

    printf "0x%x" $newmask_eth0 > $EDMA_ETH0_PORTBMP
    printf "0x%x" $newmask_eth1 > $EDMA_ETH1_PORTBMP
}

#moves the given port to the eth0 vlan (2) or the eth1 vlan (1)
#and removes it from the other via swconfig
lp_moveportswconfig() {
    local port=$1
    local dest_iface=$2
    local removefromvlan
    local addtovlan
    local isovlan=$(( $port + $LP_VLAN_OFFSET ))
    local isoiface=$(lp_interfacebyport $port)

    if [ $dest_iface = "eth0" ]; then
        removefromvlan=1
        addtovlan=2
    elif [ $dest_iface = "eth1" ]; then
        removefromvlan=2
        addtovlan=1
    else
        #should not get here, but if we do do not move port
        removefromvlan=3
        addtovlan=3
    fi

    local currports=$(swconfig dev switch0 vlan $removefromvlan show | grep ports | cut -d: -f2)
    local newports
    for p in $currports; do
        if [ $p != $port ]; then
            newports="$newports $p"
        fi
    done
    __lp_logdebug "swconfig dev switch0 vlan $removefromvlan set ports $newports"
    swconfig dev switch0 vlan $removefromvlan set ports $newports

    currports=$(swconfig dev switch0 vlan $addtovlan show | grep ports | cut -d: -f2)
    __lp_logdebug "swconfig dev switch0 vlan $addtovlan set ports \"$currports $port\""
    swconfig dev switch0 vlan $addtovlan set ports "$currports $port"

    #eliminate the isolation vlan and virtual interface
    __lp_logdebug "swconfig dev switch0 vlan $isovlan set ports \"\""
    swconfig dev switch0 vlan $isovlan set ports ""
    __lp_logdebug "vconfig rem $isoiface"
    vconfig rem $isoiface

    __lp_logdebug "swconfig dev switch0 set apply"
    swconfig dev switch0 set apply
}


#Move the given port to the eth0 vlan
lp_moveporttoeth0() {
    local port=$1
    __lp_loginfo "Moving switch port $port into the eth0 vlan"
    lp_moveportswconfig $port "eth0"
    lp_setedmamasks $port "eth0"
}

#Move the given port to the eth1 vlan
lp_moveporttoeth1() {
    local port=$1
    __lp_loginfo "Moving switch port $port into the eth1 vlan"
    lp_moveportswconfig $port "eth1"
    lp_setedmamasks $port "eth1"
}


#Make the hotplug call to signal the direction has been determined for
#the given port
lp_signaldirectionhotplug() {
    local port=$1
    local direction=$2
    (export PORT=$port; export EVENT="direction"; export STATE=$direction; hotplug-call switch)
}

#Called by hotplug whenever a port transtions to link-up status
lp_onlinkup() {
    local port=$1
    local role=$(lp_devicerole)
    local direction
    local current_direction=$(lp_getportdirection $port)

    logger -t repacd.lp -p user.debug "did we miss a down notification for port $port? current_direction: $current_direction"
    if [ $current_direction != $LP_DIRECTION_UNKNOWN ]; then
        logger -t repacd.lp -p user.debug "we missed a down notification for port $port so act like we didn't"
        lp_onlinkdown $port
    fi
    direction=$(lp_determinedirection $port $role)
    lp_signaldirectionhotplug $port $direction
}

#Called by hotplug whenever a port transtions to link-down status
lp_onlinkdown() {
    local port=$1
    lp_setportdb $port $LP_DIRECTION_UNKNOWN
    #isolate port back into its own iface
    lp_isolateport $port
}

#Called by hotplug whenever a port direction has been determined upstream
lp_onupstream() {
    local port=$1
    lp_setportdb $port $LP_DIRECTION_UPSTREAM
    lp_moveporttoeth0 $port
}

#Called by hotplug whenever a port direction has been determined downstream
lp_ondownstream() {
    local port=$1
    lp_setportdb $port $LP_DIRECTION_DOWNSTREAM
    lp_moveporttoeth1 $port
}

repacd_lp_init() {
    local nports=$(lp_numports)
    for p in $(seq 1 $nports); do
        lp_isolateport $p
        lp_setportdb $p $LP_DIRECTION_UNKNOWN
    done
}
